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
package/bin/index.js CHANGED
@@ -27,8 +27,6 @@ import { createInterface } from 'node:readline/promises';
27
27
  import { stdin, stdout, argv, exit } from 'node:process';
28
28
 
29
29
  const here = dirname(fileURLToPath(import.meta.url));
30
- const templateFor = (type) =>
31
- resolve(here, '..', type === 'client' ? 'template' : `template-${type}`);
32
30
 
33
31
  // ── tiny ANSI palette (no chalk; one less thing to install) ───────────
34
32
  const supportsColor = stdout.isTTY && process.env.NO_COLOR === undefined;
@@ -122,6 +120,24 @@ async function pickType() {
122
120
  return (TYPES[Number(a) - 1] || TYPES[0]).key;
123
121
  }
124
122
 
123
+ // SSR only: with a database (SQLite + auto-CRUD + auth + admin) or without one
124
+ // (a markdown blog on file sources). Flags: --db / --no-db.
125
+ async function pickSsrDb() {
126
+ const flags = argv.slice(2);
127
+ if (flags.includes('--no-db')) return false;
128
+ if (flags.includes('--db')) return true;
129
+ if (!stdin.isTTY) return true;
130
+ const a = (await prompt(`${c.accent('?')} Include a database? ${c.dim('(Y/n)')} `, 'y')).toLowerCase();
131
+ return /^y(es)?$/.test(a);
132
+ }
133
+
134
+ // Resolve the template directory for a chosen project type.
135
+ function templateDir(type, ssrDb) {
136
+ if (type === 'client') return resolve(here, '..', 'template');
137
+ if (type === 'prerender') return resolve(here, '..', 'template-prerender');
138
+ return resolve(here, '..', ssrDb ? 'template-ssr' : 'template-ssr-nodb');
139
+ }
140
+
125
141
  // ── optional features ──────────────────────────────────────────────────
126
142
  // The template ships with EVERYTHING wired; excluded features are stripped
127
143
  // out of the copied files via `@spark:<name>` … `@spark:end` marker blocks
@@ -232,10 +248,14 @@ async function main() {
232
248
 
233
249
  // 3 ─ pick the project type + features, copy the template ────────────
234
250
  const type = await pickType();
235
- const features = type === 'client' ? await pickFeatures() : {};
251
+ // SSR can ship with or without a database; the choice picks the template.
252
+ const ssrDb = type === 'ssr' ? await pickSsrDb() : true;
253
+ // The prerender template is the full showcase (router, todos, demos) whose
254
+ // optional pieces are toggled via @spark markers; client and ssr ship fixed.
255
+ const features = type === 'prerender' ? await pickFeatures() : {};
236
256
  mkdirSync(targetDir, { recursive: true });
237
- cpSync(templateFor(type), targetDir, { recursive: true });
238
- if (type === 'client') applyFeatures(targetDir, features);
257
+ cpSync(templateDir(type, ssrDb), targetDir, { recursive: true });
258
+ if (type === 'prerender') applyFeatures(targetDir, features);
239
259
 
240
260
  // npm renames/strips dotfiles on publish, so the template ships them
241
261
  // with safe underscore prefixes. Restore the real names here.
@@ -253,7 +273,7 @@ async function main() {
253
273
  const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
254
274
  pkg.name = projectName;
255
275
  // Drop dependencies that belong to excluded features.
256
- for (const f of type === 'client' ? FEATURES : []) {
276
+ for (const f of type === 'prerender' ? FEATURES : []) {
257
277
  if (features[f.key]) continue;
258
278
  for (const dep of f.deps) {
259
279
  if (pkg.dependencies) delete pkg.dependencies[dep];
@@ -285,9 +305,10 @@ async function main() {
285
305
 
286
306
  // 5 ─ celebrate + print next steps ───────────────────────────────────
287
307
  const rel = relative(process.cwd(), targetDir) || '.';
288
- 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'}`;
308
+ const flavor = type === 'ssr'
309
+ ? (ssrDb ? 'SSR spark-ssr blog, SQLite + auth, zero config' : 'SSR — spark-ssr, markdown blog, no database')
310
+ : type === 'prerender' ? `prerendered showcase — router, todos, demos + ${FEATURES.filter((f) => features[f.key]).map((f) => f.key).join(', ') || 'core only'}`
311
+ : 'client-only — a single reactive counter to build on';
291
312
  stdout.write(`\n${c.green('✔')} Scaffolded ${c.bold(projectName)} in ${c.cyan(rel)} ${c.dim(`(${flavor})`)}\n\n`);
292
313
  stdout.write(`${c.bold('Next steps:')}\n`);
293
314
  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.1",
3
+ "version": "0.13.0",
4
4
  "description": "Scaffold a spark-html app — dev/build/preview on Bun",
5
5
  "type": "module",
6
6
  "bin": {
@@ -10,6 +10,7 @@
10
10
  "bin",
11
11
  "template",
12
12
  "template-ssr",
13
+ "template-ssr-nodb",
13
14
  "template-prerender"
14
15
  ],
15
16
  "engines": {
@@ -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; }