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.
Files changed (52) hide show
  1. package/bin/index.js +30 -9
  2. package/package.json +2 -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/public/style.css +113 -54
  18. package/template-ssr/spark.json +1 -1
  19. package/template-ssr-nodb/README.md +32 -0
  20. package/template-ssr-nodb/_gitignore +2 -0
  21. package/template-ssr-nodb/components/nav.html +11 -0
  22. package/template-ssr-nodb/components/post-card.html +8 -0
  23. package/template-ssr-nodb/components/theme-toggle.html +9 -0
  24. package/template-ssr-nodb/content/hello-spark.md +12 -0
  25. package/template-ssr-nodb/content/no-database.md +14 -0
  26. package/template-ssr-nodb/content/the-template-is-the-site.md +14 -0
  27. package/template-ssr-nodb/lib/post.js +23 -0
  28. package/template-ssr-nodb/package.json +17 -0
  29. package/template-ssr-nodb/pages/_layout.html +14 -0
  30. package/template-ssr-nodb/pages/about.html +18 -0
  31. package/template-ssr-nodb/pages/blog/[slug].html +27 -0
  32. package/template-ssr-nodb/pages/index.html +27 -0
  33. package/template-ssr-nodb/public/app.js +8 -0
  34. package/template-ssr-nodb/public/img/avatar.png +0 -0
  35. package/template-ssr-nodb/public/style.css +175 -0
  36. package/template-ssr-nodb/spark.json +3 -0
  37. package/template/spark.config.js +0 -72
  38. package/template-prerender/src/style.css +0 -3
  39. package/template-ssr/404.html +0 -9
  40. /package/{template → template-prerender}/public/components/about.html +0 -0
  41. /package/{template → template-prerender}/public/components/demo-await.html +0 -0
  42. /package/{template → template-prerender}/public/components/demo-image.html +0 -0
  43. /package/{template → template-prerender}/public/components/demo-persist.html +0 -0
  44. /package/{template → template-prerender}/public/components/demo-props.html +0 -0
  45. /package/{template → template-prerender}/public/components/demo-todo.html +0 -0
  46. /package/{template → template-prerender}/public/components/feature-card.html +0 -0
  47. /package/{template → template-prerender}/public/components/footer.html +0 -0
  48. /package/{template → template-prerender}/public/components/home.html +0 -0
  49. /package/{template → template-prerender}/public/components/nav.html +0 -0
  50. /package/{template → template-prerender}/public/icon.png +0 -0
  51. /package/{template → template-prerender}/public/lib/format.js +0 -0
  52. /package/{template → template-prerender}/public/sample.jpg +0 -0
@@ -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
@@ -1,13 +1,214 @@
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" />
6
- <title>Spark Static App</title>
7
- <link rel="stylesheet" href="/src/style.css" />
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/hero"></div>
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-static-app",
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
- <main>
2
- <h1>⚡ {greeting}</h1>
3
- <p>This page is prerendered at build time — view-source on the deployed
4
- site and the content is right there. Edit this component and save.</p>
5
- <button onclick={cheer}>Clicked {count} times</button>
6
- </main>
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
- let greeting = 'HTML that reacts';
26
+ import { capitalize, pluralize } from '../lib/format.js';
27
+
10
28
  let count = 0;
11
- let pageTitle = 'Spark Static App';
12
- let pageDescription = 'A prerendered spark-html starter — SEO-ready static HTML that hydrates into a reactive app.';
13
- function cheer() { count++; }
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
- // Static site, the Spark way: `spark dev` serves components raw with HMR;
4
- // `spark build` copies public/, bundles the entry, then prerender() runs the
5
- // REAL app at build time and writes fully-rendered HTML into dist/ crawlers
6
- // and AI tools read real content, the browser hydrates over it.
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: [prerender({ pages: ['index.html'] })],
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 { mount } from 'spark-html';
2
- mount();
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