create-spark-html-app 0.11.1 → 0.12.0
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/bin/index.js +7 -5
- package/package.json +1 -1
- package/template/README.md +9 -68
- package/template/_gitignore +0 -2
- package/template/index.html +4 -204
- package/template/package.json +2 -12
- package/template/public/components/hero.html +12 -64
- package/template/src/main.js +2 -61
- package/template/src/style.css +32 -0
- package/template-prerender/README.md +71 -8
- package/template-prerender/_gitignore +2 -0
- package/template-prerender/index.html +206 -5
- package/template-prerender/package.json +12 -3
- package/template-prerender/public/components/hero.html +71 -10
- package/template-prerender/spark.config.js +68 -5
- package/template-prerender/src/main.js +61 -2
- package/template-ssr/public/style.css +113 -54
- package/template-ssr/spark.json +1 -1
- package/template/spark.config.js +0 -72
- package/template-prerender/src/style.css +0 -3
- /package/{template → template-prerender}/public/components/about.html +0 -0
- /package/{template → template-prerender}/public/components/demo-await.html +0 -0
- /package/{template → template-prerender}/public/components/demo-image.html +0 -0
- /package/{template → template-prerender}/public/components/demo-persist.html +0 -0
- /package/{template → template-prerender}/public/components/demo-props.html +0 -0
- /package/{template → template-prerender}/public/components/demo-todo.html +0 -0
- /package/{template → template-prerender}/public/components/feature-card.html +0 -0
- /package/{template → template-prerender}/public/components/footer.html +0 -0
- /package/{template → template-prerender}/public/components/home.html +0 -0
- /package/{template → template-prerender}/public/components/nav.html +0 -0
- /package/{template → template-prerender}/public/icon.png +0 -0
- /package/{template → template-prerender}/public/lib/format.js +0 -0
- /package/{template → template-prerender}/public/sample.jpg +0 -0
|
@@ -1,13 +1,214 @@
|
|
|
1
1
|
<!doctype html>
|
|
2
2
|
<html lang="en">
|
|
3
3
|
<head>
|
|
4
|
-
<meta charset="
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
-
<title>Spark
|
|
7
|
-
<
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Spark App</title>
|
|
7
|
+
<meta name="description" content="A reactive app built with Spark — single-file HTML components, no compiler, no virtual DOM." />
|
|
8
|
+
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>⚡</text></svg>" />
|
|
9
|
+
<!-- Fonts are injected by spark-html-font/bun (see spark.config.js):
|
|
10
|
+
preconnects, the stylesheet, and a size-adjusted fallback face so the
|
|
11
|
+
swap never moves the page. -->
|
|
12
|
+
<style>
|
|
13
|
+
/* Design system — the same palette and monospace type as the Spark
|
|
14
|
+
website. Tokens + base + a shared card/button/input system live here in
|
|
15
|
+
<head> so they apply globally before any component boots. Light & dark
|
|
16
|
+
are driven by [data-theme] (spark-html-theme writes it). */
|
|
17
|
+
*,
|
|
18
|
+
*::before,
|
|
19
|
+
*::after {
|
|
20
|
+
box-sizing: border-box;
|
|
21
|
+
margin: 0;
|
|
22
|
+
padding: 0;
|
|
23
|
+
}
|
|
24
|
+
:root {
|
|
25
|
+
--bg: #000;
|
|
26
|
+
--surface: #0a0a0a;
|
|
27
|
+
--surface-2: #101014;
|
|
28
|
+
--border: #1a1a1a;
|
|
29
|
+
--border-strong: #333;
|
|
30
|
+
--text: #fff;
|
|
31
|
+
--muted: #888;
|
|
32
|
+
--muted-dim: #555;
|
|
33
|
+
--spark: #ffd24a;
|
|
34
|
+
--spark-ink: #ffd24a;
|
|
35
|
+
--danger: #ff6b6b;
|
|
36
|
+
--radius: 12px;
|
|
37
|
+
/* the injected --font-jetbrains-mono stack includes the fallback face;
|
|
38
|
+
the var() fallback list covers a config without the font step */
|
|
39
|
+
--font: var(--font-jetbrains-mono, "JetBrains Mono", ui-monospace, monospace);
|
|
40
|
+
}
|
|
41
|
+
[data-theme="light"] {
|
|
42
|
+
--bg: #fff;
|
|
43
|
+
--surface: #fafafa;
|
|
44
|
+
--surface-2: #f4f4f5;
|
|
45
|
+
--border: #ededed;
|
|
46
|
+
--border-strong: #d4d4d4;
|
|
47
|
+
--text: #1a1a1a;
|
|
48
|
+
--muted: #666;
|
|
49
|
+
--muted-dim: #999;
|
|
50
|
+
--spark: #ffd24a;
|
|
51
|
+
--spark-ink: #9a6a00;
|
|
52
|
+
--danger: #d63b3b;
|
|
53
|
+
}
|
|
54
|
+
html {
|
|
55
|
+
scroll-behavior: smooth;
|
|
56
|
+
}
|
|
57
|
+
body {
|
|
58
|
+
font-family: var(--font);
|
|
59
|
+
background: var(--bg);
|
|
60
|
+
color: var(--text);
|
|
61
|
+
line-height: 1.6;
|
|
62
|
+
-webkit-font-smoothing: antialiased;
|
|
63
|
+
min-height: 100vh;
|
|
64
|
+
display: flex;
|
|
65
|
+
flex-direction: column;
|
|
66
|
+
}
|
|
67
|
+
::selection {
|
|
68
|
+
background: var(--spark);
|
|
69
|
+
color: #000;
|
|
70
|
+
}
|
|
71
|
+
[data-theme="light"] ::selection {
|
|
72
|
+
background: #ffe9a8;
|
|
73
|
+
color: #111;
|
|
74
|
+
}
|
|
75
|
+
a {
|
|
76
|
+
color: var(--spark-ink);
|
|
77
|
+
text-decoration: none;
|
|
78
|
+
}
|
|
79
|
+
[hidden] {
|
|
80
|
+
display: none !important;
|
|
81
|
+
}
|
|
82
|
+
.routes {
|
|
83
|
+
flex: 1;
|
|
84
|
+
width: 100%;
|
|
85
|
+
max-width: 960px;
|
|
86
|
+
margin: 0 auto;
|
|
87
|
+
padding: 0 24px;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/* Shared component system — components lean on these instead of each
|
|
91
|
+
redefining a card/button/input. */
|
|
92
|
+
.card {
|
|
93
|
+
background: var(--surface);
|
|
94
|
+
border: 1px solid var(--border);
|
|
95
|
+
border-radius: var(--radius);
|
|
96
|
+
padding: 22px;
|
|
97
|
+
}
|
|
98
|
+
.card h2 {
|
|
99
|
+
font-size: 16px;
|
|
100
|
+
font-weight: 700;
|
|
101
|
+
margin-bottom: 4px;
|
|
102
|
+
letter-spacing: -0.01em;
|
|
103
|
+
display: flex;
|
|
104
|
+
align-items: center;
|
|
105
|
+
gap: 8px;
|
|
106
|
+
flex-wrap: wrap;
|
|
107
|
+
}
|
|
108
|
+
.tag {
|
|
109
|
+
font-size: 10px;
|
|
110
|
+
font-weight: 500;
|
|
111
|
+
color: var(--spark-ink);
|
|
112
|
+
border: 1px solid var(--border-strong);
|
|
113
|
+
padding: 1px 7px;
|
|
114
|
+
border-radius: 999px;
|
|
115
|
+
}
|
|
116
|
+
.hint {
|
|
117
|
+
font-size: 12.5px;
|
|
118
|
+
color: var(--muted);
|
|
119
|
+
margin-bottom: 16px;
|
|
120
|
+
line-height: 1.5;
|
|
121
|
+
}
|
|
122
|
+
code {
|
|
123
|
+
background: var(--surface-2);
|
|
124
|
+
color: var(--spark-ink);
|
|
125
|
+
padding: 1px 6px;
|
|
126
|
+
border-radius: 4px;
|
|
127
|
+
font-size: 12px;
|
|
128
|
+
}
|
|
129
|
+
.row {
|
|
130
|
+
display: flex;
|
|
131
|
+
gap: 8px;
|
|
132
|
+
align-items: center;
|
|
133
|
+
flex-wrap: wrap;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
button,
|
|
137
|
+
.btn {
|
|
138
|
+
font-family: inherit;
|
|
139
|
+
font-size: 13px;
|
|
140
|
+
cursor: pointer;
|
|
141
|
+
padding: 8px 15px;
|
|
142
|
+
border-radius: 8px;
|
|
143
|
+
border: 1px solid var(--border-strong);
|
|
144
|
+
background: var(--surface-2);
|
|
145
|
+
color: var(--text);
|
|
146
|
+
transition:
|
|
147
|
+
border-color 0.12s,
|
|
148
|
+
background 0.12s,
|
|
149
|
+
color 0.12s,
|
|
150
|
+
transform 0.08s;
|
|
151
|
+
}
|
|
152
|
+
button:hover:not(:disabled) {
|
|
153
|
+
border-color: var(--spark);
|
|
154
|
+
color: var(--spark-ink);
|
|
155
|
+
}
|
|
156
|
+
button:active:not(:disabled) {
|
|
157
|
+
transform: scale(0.97);
|
|
158
|
+
}
|
|
159
|
+
button:disabled {
|
|
160
|
+
opacity: 0.4;
|
|
161
|
+
cursor: not-allowed;
|
|
162
|
+
}
|
|
163
|
+
button.primary {
|
|
164
|
+
background: var(--spark);
|
|
165
|
+
color: #000;
|
|
166
|
+
border-color: var(--spark);
|
|
167
|
+
font-weight: 700;
|
|
168
|
+
}
|
|
169
|
+
button.primary:hover:not(:disabled) {
|
|
170
|
+
color: #000;
|
|
171
|
+
filter: brightness(1.06);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
label {
|
|
175
|
+
display: flex;
|
|
176
|
+
flex-direction: column;
|
|
177
|
+
gap: 4px;
|
|
178
|
+
font-size: 12px;
|
|
179
|
+
color: var(--muted);
|
|
180
|
+
}
|
|
181
|
+
input,
|
|
182
|
+
textarea {
|
|
183
|
+
font-family: inherit;
|
|
184
|
+
font-size: 14px;
|
|
185
|
+
width: 100%;
|
|
186
|
+
background: var(--bg);
|
|
187
|
+
color: var(--text);
|
|
188
|
+
border: 1px solid var(--border-strong);
|
|
189
|
+
border-radius: 8px;
|
|
190
|
+
padding: 9px 12px;
|
|
191
|
+
}
|
|
192
|
+
input:focus,
|
|
193
|
+
textarea:focus {
|
|
194
|
+
outline: none;
|
|
195
|
+
border-color: var(--spark);
|
|
196
|
+
}
|
|
197
|
+
</style>
|
|
8
198
|
</head>
|
|
9
199
|
<body>
|
|
10
|
-
<div import="components/
|
|
200
|
+
<div import="components/nav"></div>
|
|
201
|
+
<main class="routes">
|
|
202
|
+
<!-- @spark:router -->
|
|
203
|
+
<template route="/"><div import="components/home"></div></template>
|
|
204
|
+
<template route="/about"><div import="components/about"></div></template>
|
|
205
|
+
<!-- @spark:end -->
|
|
206
|
+
<!-- @spark:!router -->
|
|
207
|
+
<div import="components/home"></div>
|
|
208
|
+
<!-- @spark:end -->
|
|
209
|
+
</main>
|
|
210
|
+
<div import="components/footer"></div>
|
|
211
|
+
|
|
11
212
|
<script type="module" src="/src/main.js"></script>
|
|
12
213
|
</body>
|
|
13
214
|
</html>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"name": "spark-
|
|
2
|
+
"name": "spark-app",
|
|
3
3
|
"private": true,
|
|
4
4
|
"version": "0.0.0",
|
|
5
5
|
"type": "module",
|
|
@@ -9,10 +9,19 @@
|
|
|
9
9
|
"preview": "spark preview"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"spark-html": "latest"
|
|
12
|
+
"spark-html": "latest",
|
|
13
|
+
"spark-html-router": "latest",
|
|
14
|
+
"spark-html-theme": "latest",
|
|
15
|
+
"spark-html-head": "latest",
|
|
16
|
+
"spark-html-persist": "latest",
|
|
17
|
+
"spark-html-sri": "latest",
|
|
18
|
+
"spark-html-manifest": "latest"
|
|
13
19
|
},
|
|
14
20
|
"devDependencies": {
|
|
15
21
|
"spark-html-bun": "latest",
|
|
16
|
-
"spark-prerender": "latest"
|
|
22
|
+
"spark-prerender": "latest",
|
|
23
|
+
"spark-html-devtools": "latest",
|
|
24
|
+
"spark-html-image": "latest",
|
|
25
|
+
"spark-html-font": "latest"
|
|
17
26
|
}
|
|
18
27
|
}
|
|
@@ -1,14 +1,75 @@
|
|
|
1
|
-
<
|
|
2
|
-
<
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
<
|
|
6
|
-
|
|
1
|
+
<header class="hero">
|
|
2
|
+
<span class="bolt" :class="igniting ? 'bolt lit' : 'bolt'" onclick="{ignite}" title="Strike the bolt">⚡</span>
|
|
3
|
+
|
|
4
|
+
<h1>HTML that <span class="grad">reacts</span>.</h1>
|
|
5
|
+
<p class="tagline">
|
|
6
|
+
Single-file reactive components — no compiler, no virtual DOM, no build step.
|
|
7
|
+
Everything below is live the moment the page loads.
|
|
8
|
+
</p>
|
|
9
|
+
|
|
10
|
+
<div class="counter">
|
|
11
|
+
<button class="round" onclick="{count = Math.max(0, count - 1)}" :disabled="count <= 0" aria-label="decrement">–</button>
|
|
12
|
+
<div class="readout">
|
|
13
|
+
<span class="num">{count}</span>
|
|
14
|
+
<span class="sub">doubled is {doubled} · {capitalize(mood)}</span>
|
|
15
|
+
</div>
|
|
16
|
+
<button class="round plus" onclick="{count++}" aria-label="increment">+</button>
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<p class="store-line">
|
|
20
|
+
You've struck the bolt <strong>{app.sparks}</strong>
|
|
21
|
+
{pluralize(app.sparks, 'time')} — that lives in a shared store.
|
|
22
|
+
</p>
|
|
23
|
+
</header>
|
|
7
24
|
|
|
8
25
|
<script>
|
|
9
|
-
|
|
26
|
+
import { capitalize, pluralize } from '../lib/format.js';
|
|
27
|
+
|
|
10
28
|
let count = 0;
|
|
11
|
-
let
|
|
12
|
-
|
|
13
|
-
|
|
29
|
+
let igniting = false;
|
|
30
|
+
|
|
31
|
+
$: doubled = count * 2;
|
|
32
|
+
$: mood = count === 0 ? 'a calm start' : count < 5 ? 'warming up' : 'on fire';
|
|
33
|
+
|
|
34
|
+
const app = useStore('app');
|
|
35
|
+
|
|
36
|
+
function ignite() {
|
|
37
|
+
app.sparks++;
|
|
38
|
+
igniting = true;
|
|
39
|
+
setTimeout(() => { igniting = false; }, 450);
|
|
40
|
+
}
|
|
14
41
|
</script>
|
|
42
|
+
|
|
43
|
+
<style>
|
|
44
|
+
.hero { display: flex; flex-direction: column; align-items: center; text-align: center; gap: 16px; padding-top: 32px; }
|
|
45
|
+
.bolt {
|
|
46
|
+
font-size: 32px; line-height: 1; cursor: pointer; user-select: none;
|
|
47
|
+
filter: drop-shadow(0 0 14px rgba(255, 210, 74, .45));
|
|
48
|
+
transition: transform .2s ease, filter .2s ease;
|
|
49
|
+
}
|
|
50
|
+
.bolt:hover { transform: translateY(-1px) scale(1.06); }
|
|
51
|
+
.bolt.lit { animation: zap .45s ease; }
|
|
52
|
+
@keyframes zap {
|
|
53
|
+
0% { transform: scale(1); filter: drop-shadow(0 0 14px rgba(255, 210, 74, .45)); }
|
|
54
|
+
40% { transform: scale(1.35) rotate(-8deg); filter: drop-shadow(0 0 34px rgba(255, 210, 74, .95)); }
|
|
55
|
+
100% { transform: scale(1); filter: drop-shadow(0 0 14px rgba(255, 210, 74, .45)); }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
h1 { font-size: clamp(32px, 6vw, 48px); font-weight: 800; letter-spacing: -.03em; }
|
|
59
|
+
.grad {
|
|
60
|
+
background: linear-gradient(110deg, var(--text), var(--spark));
|
|
61
|
+
-webkit-background-clip: text; background-clip: text; color: transparent;
|
|
62
|
+
}
|
|
63
|
+
.tagline { max-width: 480px; color: var(--muted); font-size: 14px; }
|
|
64
|
+
|
|
65
|
+
.counter { display: flex; align-items: center; justify-content: center; gap: 22px; margin-top: 8px; }
|
|
66
|
+
.round {
|
|
67
|
+
width: 44px; height: 44px; border-radius: 50%; font-size: 22px; padding: 0;
|
|
68
|
+
}
|
|
69
|
+
.round.plus { background: var(--surface-2); }
|
|
70
|
+
.readout { min-width: 130px; }
|
|
71
|
+
.num { display: block; font-size: 42px; font-weight: 800; font-variant-numeric: tabular-nums; line-height: 1.1; }
|
|
72
|
+
.sub { font-size: 11px; color: var(--muted); }
|
|
73
|
+
.store-line { font-size: 13px; color: var(--muted); margin-top: 4px; }
|
|
74
|
+
.store-line strong { color: var(--spark-ink); }
|
|
75
|
+
</style>
|
|
@@ -1,9 +1,72 @@
|
|
|
1
1
|
import prerender from 'spark-prerender/bun';
|
|
2
|
+
// @spark:theme
|
|
3
|
+
import theme from 'spark-html-theme/bun';
|
|
4
|
+
// @spark:end
|
|
5
|
+
// @spark:font
|
|
6
|
+
import font from 'spark-html-font/bun';
|
|
7
|
+
// @spark:end
|
|
8
|
+
// @spark:image
|
|
9
|
+
import image from 'spark-html-image/bun';
|
|
10
|
+
// @spark:end
|
|
11
|
+
// @spark:pwa
|
|
12
|
+
import manifest from 'spark-html-manifest/bun';
|
|
13
|
+
// @spark:end
|
|
14
|
+
// @spark:sri
|
|
15
|
+
import sri from 'spark-html-sri/bun';
|
|
16
|
+
// @spark:end
|
|
2
17
|
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
18
|
+
// Spark needs no build step — spark-html-bun is just a fast dev server and a
|
|
19
|
+
// bundler for your app shell. `spark dev` serves component fragments raw and
|
|
20
|
+
// hot-reloads only the edited component; components live in public/ so they
|
|
21
|
+
// ship verbatim to the production build too.
|
|
22
|
+
//
|
|
23
|
+
// The `pipeline` runs in order after `spark build` copies public/ and bundles
|
|
24
|
+
// the entry. Order matters: prerender() first (it writes one HTML file per
|
|
25
|
+
// route), then the steps that rewrite those pages — sri() must be last so it
|
|
26
|
+
// hashes the final bytes.
|
|
27
|
+
//
|
|
28
|
+
// `prerender()` makes `bun run build` SEO-friendly: it runs your app at build
|
|
29
|
+
// time and writes fully-rendered HTML into dist/ (crawlers and AI tools read
|
|
30
|
+
// real content; the browser still hydrates over it), plus sitemap.xml +
|
|
31
|
+
// robots.txt. Remove it if you don't need SEO.
|
|
7
32
|
export default {
|
|
8
|
-
pipeline: [
|
|
33
|
+
pipeline: [
|
|
34
|
+
prerender({ pages: ['index.html'] }),
|
|
35
|
+
// @spark:theme
|
|
36
|
+
// Bakes the tiny no-flash script into each page (dev too) so the saved /
|
|
37
|
+
// OS theme is on <html> before first paint — no wrong-theme flash on load.
|
|
38
|
+
theme(),
|
|
39
|
+
// @spark:end
|
|
40
|
+
// @spark:font
|
|
41
|
+
// Preconnect + the Google stylesheet + a size-adjusted local fallback
|
|
42
|
+
// face, baked into each page (dev too) — the font swap never moves or
|
|
43
|
+
// visibly reflows the page.
|
|
44
|
+
font({
|
|
45
|
+
fallback: ['ui-monospace', 'monospace'],
|
|
46
|
+
fonts: [{ family: 'JetBrains Mono', google: true, weights: [300, 400, 500, 600, 700, 800] }],
|
|
47
|
+
}),
|
|
48
|
+
// @spark:end
|
|
49
|
+
// @spark:image
|
|
50
|
+
// Every <img> in pages and components: converted to webp/avif at multiple
|
|
51
|
+
// widths, srcset + width/height added (no layout shift), loading="lazy".
|
|
52
|
+
// Zero config.
|
|
53
|
+
image(),
|
|
54
|
+
// @spark:end
|
|
55
|
+
// @spark:pwa
|
|
56
|
+
// One config → manifest.webmanifest + resized icons + <head> tags + an
|
|
57
|
+
// offline app-shell service worker (registered automatically).
|
|
58
|
+
manifest({
|
|
59
|
+
name: 'Spark App',
|
|
60
|
+
themeColor: '#ffd24a',
|
|
61
|
+
icon: 'public/icon.png',
|
|
62
|
+
offline: true,
|
|
63
|
+
}),
|
|
64
|
+
// @spark:end
|
|
65
|
+
// @spark:sri
|
|
66
|
+
// Hashes every built asset + component, stamps integrity/crossorigin onto
|
|
67
|
+
// script/link tags, and bakes the verify manifest into each page. Keep it
|
|
68
|
+
// LAST so it sees the final pages.
|
|
69
|
+
sri(),
|
|
70
|
+
// @spark:end
|
|
71
|
+
],
|
|
9
72
|
};
|
|
@@ -1,2 +1,61 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { store } from "spark-html";
|
|
2
|
+
// @spark:!router
|
|
3
|
+
import { mount } from "spark-html";
|
|
4
|
+
// @spark:end
|
|
5
|
+
// @spark:router
|
|
6
|
+
import { router } from "spark-html-router";
|
|
7
|
+
// @spark:end
|
|
8
|
+
// @spark:theme
|
|
9
|
+
import { theme } from "spark-html-theme";
|
|
10
|
+
// @spark:end
|
|
11
|
+
import { head } from "spark-html-head";
|
|
12
|
+
import { persist } from "spark-html-persist";
|
|
13
|
+
// @spark:sri
|
|
14
|
+
import { sri } from "spark-html-sri";
|
|
15
|
+
// @spark:end
|
|
16
|
+
import { devtools } from "spark-html-devtools";
|
|
17
|
+
|
|
18
|
+
const dev = import.meta.env?.DEV;
|
|
19
|
+
if (dev) devtools(); // dev only
|
|
20
|
+
|
|
21
|
+
// @spark:sri
|
|
22
|
+
// Subresource Integrity: verifies every component fetch against the
|
|
23
|
+
// manifest the build baked into the page, and allow-lists remote URL
|
|
24
|
+
// imports (TOFU). Fails open on localhost, enforces in production.
|
|
25
|
+
sri();
|
|
26
|
+
// @spark:end
|
|
27
|
+
|
|
28
|
+
head({
|
|
29
|
+
title: { "/": "Home", "/about": "About", "*": "Not found" },
|
|
30
|
+
titleTemplate: (t) => `${t} · My Site`,
|
|
31
|
+
meta: { description: (path) => `The ${path} page` },
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Shared stores connect components without providers or prop drilling.
|
|
35
|
+
store("app", { sparks: 0 });
|
|
36
|
+
|
|
37
|
+
// A store that survives reloads — hydrates from localStorage on boot,
|
|
38
|
+
// saves on every change (see components/demo-persist.html).
|
|
39
|
+
persist("prefs", { name: "", visits: 0 });
|
|
40
|
+
|
|
41
|
+
// @spark:theme
|
|
42
|
+
// One-line dark/light/system theming (the ⚡ logo toggles it).
|
|
43
|
+
theme();
|
|
44
|
+
// @spark:end
|
|
45
|
+
|
|
46
|
+
// @spark:pwa
|
|
47
|
+
// PWA: manifest.webmanifest, icons, and the offline app-shell worker are
|
|
48
|
+
// generated by spark-html-manifest/bun (see spark.config.js) — the worker
|
|
49
|
+
// registration is injected into every built page automatically.
|
|
50
|
+
// Want offline-capable CDN component imports too? See spark-html-offline
|
|
51
|
+
// (a page registers one worker per scope, so pick the one you need).
|
|
52
|
+
// @spark:end
|
|
53
|
+
|
|
54
|
+
// @spark:router
|
|
55
|
+
// Client-side router: reads <template route> blocks, intercepts <a> clicks,
|
|
56
|
+
// and manages SPA navigation. Call it once — replaces mount().
|
|
57
|
+
router({ devOverlay: dev, quiet: !dev });
|
|
58
|
+
// @spark:end
|
|
59
|
+
// @spark:!router
|
|
60
|
+
mount(document.body);
|
|
61
|
+
// @spark:end
|