create-spark-html-app 0.11.0 → 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/components/nav.html +6 -5
- package/template-ssr/pages/_layout.html +1 -1
- 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
package/bin/index.js
CHANGED
|
@@ -232,10 +232,12 @@ async function main() {
|
|
|
232
232
|
|
|
233
233
|
// 3 ─ pick the project type + features, copy the template ────────────
|
|
234
234
|
const type = await pickType();
|
|
235
|
-
|
|
235
|
+
// The prerender template is the full showcase (router, todos, demos) whose
|
|
236
|
+
// optional pieces are toggled via @spark markers; client and ssr ship fixed.
|
|
237
|
+
const features = type === 'prerender' ? await pickFeatures() : {};
|
|
236
238
|
mkdirSync(targetDir, { recursive: true });
|
|
237
239
|
cpSync(templateFor(type), targetDir, { recursive: true });
|
|
238
|
-
if (type === '
|
|
240
|
+
if (type === 'prerender') applyFeatures(targetDir, features);
|
|
239
241
|
|
|
240
242
|
// npm renames/strips dotfiles on publish, so the template ships them
|
|
241
243
|
// with safe underscore prefixes. Restore the real names here.
|
|
@@ -253,7 +255,7 @@ async function main() {
|
|
|
253
255
|
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
254
256
|
pkg.name = projectName;
|
|
255
257
|
// Drop dependencies that belong to excluded features.
|
|
256
|
-
for (const f of type === '
|
|
258
|
+
for (const f of type === 'prerender' ? FEATURES : []) {
|
|
257
259
|
if (features[f.key]) continue;
|
|
258
260
|
for (const dep of f.deps) {
|
|
259
261
|
if (pkg.dependencies) delete pkg.dependencies[dep];
|
|
@@ -286,8 +288,8 @@ async function main() {
|
|
|
286
288
|
// 5 ─ celebrate + print next steps ───────────────────────────────────
|
|
287
289
|
const rel = relative(process.cwd(), targetDir) || '.';
|
|
288
290
|
const flavor = type === 'ssr' ? 'SSR — spark-ssr, zero config, no build'
|
|
289
|
-
: type === 'prerender' ?
|
|
290
|
-
:
|
|
291
|
+
: type === 'prerender' ? `prerendered showcase — router, todos, demos + ${FEATURES.filter((f) => features[f.key]).map((f) => f.key).join(', ') || 'core only'}`
|
|
292
|
+
: 'client-only — a single reactive counter to build on';
|
|
291
293
|
stdout.write(`\n${c.green('✔')} Scaffolded ${c.bold(projectName)} in ${c.cyan(rel)} ${c.dim(`(${flavor})`)}\n\n`);
|
|
292
294
|
stdout.write(`${c.bold('Next steps:')}\n`);
|
|
293
295
|
if (rel !== '.') stdout.write(` ${c.dim('1.')} cd ${rel}\n`);
|
package/package.json
CHANGED
package/template/README.md
CHANGED
|
@@ -1,76 +1,17 @@
|
|
|
1
1
|
# ⚡ Spark App
|
|
2
2
|
|
|
3
|
-
A
|
|
4
|
-
|
|
3
|
+
A minimal client-side app built with
|
|
4
|
+
[spark-html](https://github.com/wilkinnovo/spark-html) — single-file HTML
|
|
5
|
+
components with built-in reactivity. No compiler, no virtual DOM, no build step.
|
|
5
6
|
|
|
6
|
-
|
|
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
|
+
Edit `public/components/hero.html` and save to see it update instantly.
|
|
15
8
|
|
|
16
9
|
```bash
|
|
17
10
|
bun install
|
|
18
|
-
bun run dev
|
|
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
|
|
11
|
+
bun run dev # dev server with HMR
|
|
12
|
+
bun run build # bundle → dist/, deploy anywhere
|
|
13
|
+
bun run preview # preview the production build
|
|
29
14
|
```
|
|
30
15
|
|
|
31
|
-
|
|
32
|
-
|
|
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.
|
|
16
|
+
Want server rendering or prerendered static HTML? Scaffold again and pick the
|
|
17
|
+
**SSR** or **Prerender** project type.
|
package/template/_gitignore
CHANGED
package/template/index.html
CHANGED
|
@@ -1,214 +1,14 @@
|
|
|
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
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
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
7
|
<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
|
-
|
|
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
|
+
<link rel="stylesheet" href="/src/style.css" />
|
|
198
9
|
</head>
|
|
199
10
|
<body>
|
|
200
|
-
<div import="components/
|
|
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
|
+
<div import="components/hero"></div>
|
|
212
12
|
<script type="module" src="/src/main.js"></script>
|
|
213
13
|
</body>
|
|
214
14
|
</html>
|
package/template/package.json
CHANGED
|
@@ -9,19 +9,9 @@
|
|
|
9
9
|
"preview": "spark preview"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
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"
|
|
12
|
+
"spark-html": "latest"
|
|
19
13
|
},
|
|
20
14
|
"devDependencies": {
|
|
21
|
-
"spark-html-bun": "latest"
|
|
22
|
-
"spark-prerender": "latest",
|
|
23
|
-
"spark-html-devtools": "latest",
|
|
24
|
-
"spark-html-image": "latest",
|
|
25
|
-
"spark-html-font": "latest"
|
|
15
|
+
"spark-html-bun": "latest"
|
|
26
16
|
}
|
|
27
17
|
}
|
|
@@ -1,75 +1,23 @@
|
|
|
1
|
-
<
|
|
2
|
-
<
|
|
3
|
-
|
|
4
|
-
<h1>HTML that <span class="grad">reacts</span>.</h1>
|
|
1
|
+
<main>
|
|
2
|
+
<h1>⚡ {greeting}</h1>
|
|
5
3
|
<p class="tagline">
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
A single-file reactive component — no compiler, no virtual DOM, no build
|
|
5
|
+
step. Edit this file and save; the page updates instantly.
|
|
8
6
|
</p>
|
|
9
7
|
|
|
10
8
|
<div class="counter">
|
|
11
|
-
<button
|
|
12
|
-
<
|
|
13
|
-
|
|
14
|
-
<span class="sub">doubled is {doubled} · {capitalize(mood)}</span>
|
|
15
|
-
</div>
|
|
16
|
-
<button class="round plus" onclick="{count++}" aria-label="increment">+</button>
|
|
9
|
+
<button onclick={dec} aria-label="decrement">–</button>
|
|
10
|
+
<span class="count">{count}</span>
|
|
11
|
+
<button onclick={inc} aria-label="increment">+</button>
|
|
17
12
|
</div>
|
|
18
13
|
|
|
19
|
-
<p class="
|
|
20
|
-
|
|
21
|
-
{pluralize(app.sparks, 'time')} — that lives in a shared store.
|
|
22
|
-
</p>
|
|
23
|
-
</header>
|
|
14
|
+
<p class="doubled" :hidden="count === 0">doubled is {doubled}</p>
|
|
15
|
+
</main>
|
|
24
16
|
|
|
25
17
|
<script>
|
|
26
|
-
|
|
27
|
-
|
|
18
|
+
let greeting = 'HTML that reacts';
|
|
28
19
|
let count = 0;
|
|
29
|
-
let igniting = false;
|
|
30
|
-
|
|
31
20
|
$: doubled = count * 2;
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const app = useStore('app');
|
|
35
|
-
|
|
36
|
-
function ignite() {
|
|
37
|
-
app.sparks++;
|
|
38
|
-
igniting = true;
|
|
39
|
-
setTimeout(() => { igniting = false; }, 450);
|
|
40
|
-
}
|
|
21
|
+
function inc() { count++; }
|
|
22
|
+
function dec() { count--; }
|
|
41
23
|
</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>
|
package/template/src/main.js
CHANGED
|
@@ -1,61 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
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
|
|
1
|
+
import { mount } from 'spark-html';
|
|
2
|
+
mount();
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
:root { color-scheme: light dark; }
|
|
2
|
+
body {
|
|
3
|
+
font-family: ui-monospace, "JetBrains Mono", "SF Mono", Menlo, monospace;
|
|
4
|
+
max-width: 36rem;
|
|
5
|
+
margin: 4rem auto;
|
|
6
|
+
padding: 0 1.5rem;
|
|
7
|
+
line-height: 1.6;
|
|
8
|
+
}
|
|
9
|
+
main { text-align: center; }
|
|
10
|
+
h1 { font-size: clamp(1.8rem, 6vw, 2.6rem); letter-spacing: -0.02em; }
|
|
11
|
+
.tagline { color: gray; font-size: 0.95rem; }
|
|
12
|
+
.counter {
|
|
13
|
+
display: flex;
|
|
14
|
+
align-items: center;
|
|
15
|
+
justify-content: center;
|
|
16
|
+
gap: 1.25rem;
|
|
17
|
+
margin: 2rem 0 0.5rem;
|
|
18
|
+
}
|
|
19
|
+
.count { font-size: 2rem; font-variant-numeric: tabular-nums; min-width: 2ch; }
|
|
20
|
+
button {
|
|
21
|
+
font: inherit;
|
|
22
|
+
width: 2.75rem;
|
|
23
|
+
height: 2.75rem;
|
|
24
|
+
border-radius: 999px;
|
|
25
|
+
border: 1px solid currentColor;
|
|
26
|
+
background: transparent;
|
|
27
|
+
color: inherit;
|
|
28
|
+
cursor: pointer;
|
|
29
|
+
font-size: 1.25rem;
|
|
30
|
+
}
|
|
31
|
+
button:active { transform: scale(0.94); }
|
|
32
|
+
.doubled { color: gray; font-size: 0.9rem; }
|
|
@@ -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.
|