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.
Files changed (35) hide show
  1. package/bin/index.js +7 -5
  2. package/package.json +1 -1
  3. package/template/README.md +9 -68
  4. package/template/_gitignore +0 -2
  5. package/template/index.html +4 -204
  6. package/template/package.json +2 -12
  7. package/template/public/components/hero.html +12 -64
  8. package/template/src/main.js +2 -61
  9. package/template/src/style.css +32 -0
  10. package/template-prerender/README.md +71 -8
  11. package/template-prerender/_gitignore +2 -0
  12. package/template-prerender/index.html +206 -5
  13. package/template-prerender/package.json +12 -3
  14. package/template-prerender/public/components/hero.html +71 -10
  15. package/template-prerender/spark.config.js +68 -5
  16. package/template-prerender/src/main.js +61 -2
  17. package/template-ssr/components/nav.html +6 -5
  18. package/template-ssr/pages/_layout.html +1 -1
  19. package/template-ssr/public/style.css +113 -54
  20. package/template-ssr/spark.json +1 -1
  21. package/template/spark.config.js +0 -72
  22. package/template-prerender/src/style.css +0 -3
  23. /package/{template → template-prerender}/public/components/about.html +0 -0
  24. /package/{template → template-prerender}/public/components/demo-await.html +0 -0
  25. /package/{template → template-prerender}/public/components/demo-image.html +0 -0
  26. /package/{template → template-prerender}/public/components/demo-persist.html +0 -0
  27. /package/{template → template-prerender}/public/components/demo-props.html +0 -0
  28. /package/{template → template-prerender}/public/components/demo-todo.html +0 -0
  29. /package/{template → template-prerender}/public/components/feature-card.html +0 -0
  30. /package/{template → template-prerender}/public/components/footer.html +0 -0
  31. /package/{template → template-prerender}/public/components/home.html +0 -0
  32. /package/{template → template-prerender}/public/components/nav.html +0 -0
  33. /package/{template → template-prerender}/public/icon.png +0 -0
  34. /package/{template → template-prerender}/public/lib/format.js +0 -0
  35. /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
- const features = type === 'client' ? await pickFeatures() : {};
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 === 'client') applyFeatures(targetDir, features);
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 === 'client' ? FEATURES : []) {
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' ? 'prerendered static site spark-prerender'
290
- : `head, persist, prerender, devtools + ${FEATURES.filter((f) => features[f.key]).map((f) => f.key).join(', ') || 'core only'}`;
291
+ : type === 'prerender' ? `prerendered showcaserouter, 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-spark-html-app",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "Scaffold a spark-html app — dev/build/preview on Bun",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,76 +1,17 @@
1
1
  # ⚡ Spark App
2
2
 
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.
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
- 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
+ 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 # dev server with HMR
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
- `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.
16
+ Want server rendering or prerendered static HTML? Scaffold again and pick the
17
+ **SSR** or **Prerender** project type.
@@ -1,4 +1,2 @@
1
1
  node_modules
2
2
  dist
3
- *.local
4
- .DS_Store
@@ -1,214 +1,14 @@
1
1
  <!doctype html>
2
2
  <html lang="en">
3
3
  <head>
4
- <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
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
- <!-- 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
+ <link rel="stylesheet" href="/src/style.css" />
198
9
  </head>
199
10
  <body>
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
+ <div import="components/hero"></div>
212
12
  <script type="module" src="/src/main.js"></script>
213
13
  </body>
214
14
  </html>
@@ -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
- <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>
1
+ <main>
2
+ <h1>⚡ {greeting}</h1>
5
3
  <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.
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 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>
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="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>
14
+ <p class="doubled" :hidden="count === 0">doubled is {doubled}</p>
15
+ </main>
24
16
 
25
17
  <script>
26
- import { capitalize, pluralize } from '../lib/format.js';
27
-
18
+ let greeting = 'HTML that reacts';
28
19
  let count = 0;
29
- let igniting = false;
30
-
31
20
  $: 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
- }
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>
@@ -1,61 +1,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
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 Static App
1
+ # ⚡ Spark App
2
2
 
3
- A prerendered static site built with
4
- [spark-html](https://github.com/wilkinnovo/spark-html) + `spark-prerender`.
5
- Components stay single-file HTML; the build writes fully-rendered pages into
6
- `dist/` (plus sitemap/robots hooks) and the browser hydrates over them.
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 # dev server with HMR
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,2 +1,4 @@
1
1
  node_modules
2
2
  dist
3
+ *.local
4
+ .DS_Store