create-spark-html-app 0.11.1 → 0.13.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 +30 -9
- package/package.json +2 -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-ssr-nodb/README.md +32 -0
- package/template-ssr-nodb/_gitignore +2 -0
- package/template-ssr-nodb/components/nav.html +11 -0
- package/template-ssr-nodb/components/post-card.html +8 -0
- package/template-ssr-nodb/components/theme-toggle.html +9 -0
- package/template-ssr-nodb/content/hello-spark.md +12 -0
- package/template-ssr-nodb/content/no-database.md +14 -0
- package/template-ssr-nodb/content/the-template-is-the-site.md +14 -0
- package/template-ssr-nodb/lib/post.js +23 -0
- package/template-ssr-nodb/package.json +17 -0
- package/template-ssr-nodb/pages/_layout.html +14 -0
- package/template-ssr-nodb/pages/about.html +18 -0
- package/template-ssr-nodb/pages/blog/[slug].html +27 -0
- package/template-ssr-nodb/pages/index.html +27 -0
- package/template-ssr-nodb/public/app.js +8 -0
- package/template-ssr-nodb/public/img/avatar.png +0 -0
- package/template-ssr-nodb/public/style.css +175 -0
- package/template-ssr-nodb/spark.json +3 -0
- package/template/spark.config.js +0 -72
- package/template-prerender/src/style.css +0 -3
- package/template-ssr/404.html +0 -9
- /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,76 @@
|
|
|
1
|
-
# ⚡ Spark
|
|
1
|
+
# ⚡ Spark App
|
|
2
2
|
|
|
3
|
-
A
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
A starter built with [spark-html](https://github.com/wilkinnovo/spark-html) — single-file
|
|
4
|
+
HTML components with built-in reactivity. No compiler, no virtual DOM, no build step.
|
|
5
|
+
|
|
6
|
+
The scaffold is a **multi-page SPA** with client-side routing, live demos, and a
|
|
7
|
+
shared design system — edit any component and save to see it update instantly.
|
|
8
|
+
Depending on the options you picked at scaffold time it also wires up
|
|
9
|
+
`spark-html-theme` (dark/light), `spark-html-image` (build-time webp/avif),
|
|
10
|
+
`spark-html-sri` (integrity checks), and `spark-html-manifest` (PWA manifest +
|
|
11
|
+
icons + offline app shell). `spark-html-head`, `spark-html-persist`,
|
|
12
|
+
`spark-prerender`, and `spark-html-devtools` are always included.
|
|
13
|
+
|
|
14
|
+
## Develop
|
|
7
15
|
|
|
8
16
|
```bash
|
|
9
17
|
bun install
|
|
10
|
-
bun run dev
|
|
11
|
-
bun run build # prerendered static output → dist/, deploy anywhere
|
|
12
|
-
bun run preview # preview the production build
|
|
18
|
+
bun run dev # dev server with HMR
|
|
13
19
|
```
|
|
20
|
+
|
|
21
|
+
In dev mode, `spark-html-devtools` adds a debugging overlay — inspect
|
|
22
|
+
component state, stores, and the mounted tree live.
|
|
23
|
+
|
|
24
|
+
## Build (SEO-ready)
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
bun run build # static output → dist/, serve anywhere
|
|
28
|
+
bun run preview # preview the production build locally
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`bun run build` is **SEO-friendly out of the box**: the `spark-prerender`
|
|
32
|
+
pipeline step runs your app at build time and writes fully-rendered HTML into
|
|
33
|
+
`dist/` — so crawlers and AI tools read real content (headings, text, links),
|
|
34
|
+
not empty placeholders. The browser still hydrates over it for full
|
|
35
|
+
interactivity.
|
|
36
|
+
|
|
37
|
+
Per-route `<title>` and `<meta>` tags are set reactively via
|
|
38
|
+
`spark-html-head` in `src/main.js` — no per-component boilerplate.
|
|
39
|
+
|
|
40
|
+
Don't need SEO? Remove the `prerender(...)` step from `spark.config.js`.
|
|
41
|
+
|
|
42
|
+
## Architecture
|
|
43
|
+
|
|
44
|
+
Client routing is set up in `src/main.js` — `router()` (from
|
|
45
|
+
`spark-html-router`) replaces `mount()` and discovers your routes from
|
|
46
|
+
`<template route>` blocks in `index.html`. Per-route `<title>` and `<meta>`
|
|
47
|
+
are handled by `head()` (from `spark-html-head`), and `spark-html-devtools`
|
|
48
|
+
provides a live debugging overlay in dev mode.
|
|
49
|
+
|
|
50
|
+
Each route is just an HTML file in `public/components/`.
|
|
51
|
+
|
|
52
|
+
## What's inside
|
|
53
|
+
|
|
54
|
+
The scaffold's components in `public/components/` each demonstrate a Spark feature
|
|
55
|
+
(all using only the published runtime — no experimental APIs):
|
|
56
|
+
|
|
57
|
+
| Component | Features shown |
|
|
58
|
+
|---|---|
|
|
59
|
+
| `nav.html` | Client routing (active link highlight via `aria-current="page"`), theme toggle via `useStore('theme')` |
|
|
60
|
+
| `hero.html` | Local state, `$:` reactive declarations, shared store (`useStore('app')`) |
|
|
61
|
+
| `home.html` | Page composition — imports `hero` + demo components for the `/` route |
|
|
62
|
+
| `about.html` | Page composition — uses `feature-card` with props and slots for the `/about` route |
|
|
63
|
+
| `demo-todo.html` | `bind:value`/`bind:checked`, `<template each>` with `key`, `$:` derived counts |
|
|
64
|
+
| `demo-props.html` | `export let` props, named `<slot>`, component composition |
|
|
65
|
+
| `demo-await.html` | `<template await>` with `once()`, `onMount`, loading/then/catch states |
|
|
66
|
+
| `feature-card.html` | Reusable card via `export let` + `<slot>`, used by `about` and `demo-props` |
|
|
67
|
+
| `footer.html` | Static content component, imported by the shell |
|
|
68
|
+
|
|
69
|
+
A component is a `.html` file with optional `<script>` and `<style>`. Top-level
|
|
70
|
+
variables are reactive state — assigning to one re-patches that component's DOM.
|
|
71
|
+
Derive values with `$:`, share state across components with `useStore(name)`, use
|
|
72
|
+
`bind:value` for two-way binds, and pass props as attributes on the `import`
|
|
73
|
+
placeholder.
|
|
74
|
+
|
|
75
|
+
See the [full docs](https://wilkinnovo.github.io/spark-html/docs) for the complete
|
|
76
|
+
template syntax reference.
|
|
@@ -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
|