create-spark-html-app 0.3.3 → 0.4.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/README.md CHANGED
@@ -25,21 +25,14 @@ Run it with no name to be prompted:
25
25
  npm create spark-html-app@latest
26
26
  ```
27
27
 
28
- ## What you get
29
-
30
- ```
31
- my-app/
32
- ├── index.html ← import placeholder + boot script
33
- ├── src/main.js ← mount() + a shared store
34
- ├── public/components/
35
- │ ├── app.html ← theme + shell
36
- │ └── welcome.html ← reactive welcome screen (counter, store, derived state)
37
- ├── vite.config.js ← spark-html/vite plugin
38
- └── package.json
39
- ```
40
28
 
29
+ The scaffold is a live tour of Spark's best features — reactive counters,
30
+ todo lists with two-way binding and keyed reconciliation, slot-based
31
+ composition, async declarative loading states, and shared stores with
32
+ derived values — all in the same monospace dark/light design as the
33
+ [Spark website](https://wilkinnovo.github.io/spark).
41
34
  Everything is plain HTML and JavaScript — no compiler, no virtual DOM, no
42
- proprietary file format. Edit a component, save, and the page reloads.
35
+ proprietary file format. Edit a component, save, and the page updates.
43
36
 
44
37
  ## License
45
38
 
package/bin/index.js CHANGED
@@ -142,12 +142,20 @@ async function main() {
142
142
  const pkgPath = join(targetDir, 'package.json');
143
143
  const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
144
144
  pkg.name = projectName;
145
- // Always start on the newest published spark-html. If the registry can't
146
- // be reached, the template's "latest" default still resolves on install.
147
- const range = await latestRange('spark-html');
148
- if (range && pkg.dependencies && pkg.dependencies['spark-html']) {
149
- pkg.dependencies['spark-html'] = range;
150
- stdout.write(`${c.dim(` using spark-html ${range}`)}\n`);
145
+ // Always start on the newest published versions of the spark packages. If the
146
+ // registry can't be reached (or a package isn't published yet), the template's
147
+ // "latest" default still resolves on install.
148
+ for (const group of ['dependencies', 'devDependencies']) {
149
+ const deps = pkg[group];
150
+ if (!deps) continue;
151
+ for (const name of Object.keys(deps)) {
152
+ if (name !== 'spark-html' && !name.startsWith('spark-html-') && name !== 'spark-prerender') continue;
153
+ const range = await latestRange(name);
154
+ if (range) {
155
+ deps[name] = range;
156
+ stdout.write(`${c.dim(` using ${name} ${range}`)}\n`);
157
+ }
158
+ }
151
159
  }
152
160
  writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
153
161
 
@@ -158,7 +166,7 @@ async function main() {
158
166
  if (rel !== '.') stdout.write(` ${c.dim('1.')} cd ${rel}\n`);
159
167
  stdout.write(` ${c.dim(rel !== '.' ? '2.' : '1.')} npm install\n`);
160
168
  stdout.write(` ${c.dim(rel !== '.' ? '3.' : '2.')} npm run dev\n\n`);
161
- stdout.write(`${BOLT} Then open the dev server and edit ${c.cyan('public/components/welcome.html')}.\n\n`);
169
+ stdout.write(`${BOLT} Then open the dev server and edit ${c.cyan('public/components/hero.html')}.\n\n`);
162
170
  }
163
171
 
164
172
  main().catch((err) => bail(err?.message || String(err)));
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "create-spark-html-app",
3
- "version": "0.3.3",
4
- "description": "Scaffold a Vite + spark-html app with a live reactive welcome screen.",
3
+ "version": "0.4.0",
4
+ "description": "Scaffold a Vite + spark-html app a live tour of Spark's best features in the website's dark/light design.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "create-spark-html-app": "bin/index.js"
@@ -3,6 +3,9 @@
3
3
  A starter built with [spark-html](https://github.com/wilkinnovo/spark) — single-file
4
4
  HTML components with built-in reactivity. No compiler, no virtual DOM, no build step.
5
5
 
6
+ The scaffold is a live tour of Spark's core features — edit any component and
7
+ save to see it update instantly.
8
+
6
9
  ## Develop
7
10
 
8
11
  ```bash
@@ -10,32 +13,46 @@ npm install
10
13
  npm run dev
11
14
  ```
12
15
 
13
- Open the dev server and edit `public/components/welcome.html`. Save, and the
14
- page reloads instantly.
15
-
16
- ## Build
16
+ ## Build (SEO-ready)
17
17
 
18
18
  ```bash
19
19
  npm run build # static output → dist/, serve anywhere
20
20
  npm run preview # preview the production build locally
21
21
  ```
22
22
 
23
- ## How it's wired
24
-
25
- ```
26
- .
27
- ├── index.html ← <div import="components/app"> + boot script
28
- ├── src/main.js ← mount() + a shared store
29
- ├── public/components/ ← your components (plain .html files)
30
- │ ├── app.html ← theme + shell
31
- │ └── welcome.html ← the live reactive welcome screen
32
- └── vite.config.js ← spark-html/vite plugin
23
+ `npm run build` is **SEO-friendly out of the box**: the `spark-prerender`
24
+ Vite plugin runs your app at build time and writes fully-rendered HTML into
25
+ `dist/` — so crawlers and AI tools read real content (headings, text, links),
26
+ not empty placeholders. The browser still hydrates over it for full
27
+ interactivity. Set page metadata as plain component state:
28
+
29
+ ```html
30
+ <script>
31
+ let pageTitle = 'My App does a thing';
32
+ let pageDescription = 'A short, crawlable description of the page.';
33
+ </script>
33
34
  ```
34
35
 
36
+ Don't need SEO? Remove the `prerender(...)` plugin from `vite.config.js`.
37
+
38
+ ## What's inside
39
+
40
+ The scaffold's components in `public/components/` each demonstrate a Spark feature
41
+ (all using only the published runtime — no experimental APIs):
42
+
43
+ | Component | Features shown |
44
+ |---|---|
45
+ | `hero.html` | Local state, `$:` reactive statements, stores (`useStore`), theme toggle |
46
+ | `demo-todo.html` | `bind:value`/`bind:checked`, `<template each>` with `key`, `$:` derived counts |
47
+ | `demo-props.html` | `export let` props, named `<slot>`, component composition |
48
+ | `demo-await.html` | `<template await>` with `once()`, `onMount`, loading/then/catch states |
49
+ | `feature-card.html` | Reusable card via `export let` + `<slot>`, used by `demo-props` |
50
+
35
51
  A component is a `.html` file with optional `<script>` and `<style>`. Top-level
36
52
  variables are reactive state — assigning to one re-patches that component's DOM.
37
- Derive values with `$:`, share state across components with `useStore(name)`,
38
- and pass props as attributes on the `import` placeholder.
53
+ Derive values with `$:`, share state across components with `useStore(name)`, use
54
+ `bind:value` for two-way binds, and pass props as attributes on the `import`
55
+ placeholder.
39
56
 
40
- See the [full docs](https://github.com/wilkinnovo/spark#readme) for the
41
- complete template syntax reference.
57
+ See the [full docs](https://wilkinnovo.github.io/spark/docs) for the complete
58
+ template syntax reference.
@@ -4,10 +4,83 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>Spark App</title>
7
- <link
8
- rel="icon"
9
- 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>"
10
- />
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
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
10
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
11
+ <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600;700;800&display=swap" rel="stylesheet" />
12
+ <script>
13
+ /* Apply the saved theme before paint — no flash of the wrong theme. */
14
+ (function () {
15
+ try {
16
+ var m = localStorage.getItem('theme-mode') || 'system';
17
+ var dark = m === 'dark' || (m === 'system' && matchMedia('(prefers-color-scheme: dark)').matches);
18
+ document.documentElement.dataset.theme = dark ? 'dark' : 'light';
19
+ } catch (e) { document.documentElement.dataset.theme = 'dark'; }
20
+ })();
21
+ </script>
22
+ <style>
23
+ /* Design system — the same palette and monospace type as the Spark
24
+ website. Tokens + base + a shared card/button/input system live here in
25
+ <head> so they apply globally before any component boots. Light & dark
26
+ are driven by [data-theme] (spark-html-theme writes it). */
27
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
28
+ :root {
29
+ --bg:#000; --surface:#0a0a0a; --surface-2:#101014;
30
+ --border:#1a1a1a; --border-strong:#333;
31
+ --text:#fff; --muted:#888; --muted-dim:#555;
32
+ --spark:#ffd24a; --spark-ink:#ffd24a; --danger:#ff6b6b;
33
+ --radius:12px;
34
+ --font:"JetBrains Mono", ui-monospace, monospace;
35
+ }
36
+ [data-theme="light"] {
37
+ --bg:#fff; --surface:#fafafa; --surface-2:#f4f4f5;
38
+ --border:#ededed; --border-strong:#d4d4d4;
39
+ --text:#1a1a1a; --muted:#666; --muted-dim:#999;
40
+ --spark:#ffd24a; --spark-ink:#9a6a00; --danger:#d63b3b;
41
+ }
42
+ html { scroll-behavior:smooth; }
43
+ body {
44
+ font-family:var(--font); background:var(--bg); color:var(--text);
45
+ line-height:1.6; -webkit-font-smoothing:antialiased; min-height:100vh;
46
+ }
47
+ ::selection { background:var(--spark); color:#000; }
48
+ [data-theme="light"] ::selection { background:#ffe9a8; color:#111; }
49
+ a { color:var(--spark-ink); text-decoration:none; }
50
+ [hidden] { display:none !important; }
51
+
52
+ /* Shared component system — components lean on these instead of each
53
+ redefining a card/button/input. */
54
+ .card {
55
+ background:var(--surface); border:1px solid var(--border);
56
+ border-radius:var(--radius); padding:22px;
57
+ }
58
+ .card h2 { font-size:16px; font-weight:700; margin-bottom:4px; letter-spacing:-.01em; display:flex; align-items:center; gap:8px; flex-wrap:wrap; }
59
+ .tag { font-size:10px; font-weight:500; color:var(--spark-ink); border:1px solid var(--border-strong); padding:1px 7px; border-radius:999px; }
60
+ .hint { font-size:12.5px; color:var(--muted); margin-bottom:16px; line-height:1.5; }
61
+ code { background:var(--surface-2); color:var(--spark-ink); padding:1px 6px; border-radius:4px; font-size:12px; }
62
+ .row { display:flex; gap:8px; align-items:center; flex-wrap:wrap; }
63
+
64
+ button, .btn {
65
+ font-family:inherit; font-size:13px; cursor:pointer;
66
+ padding:8px 15px; border-radius:8px;
67
+ border:1px solid var(--border-strong); background:var(--surface-2); color:var(--text);
68
+ transition:border-color .12s, background .12s, color .12s, transform .08s;
69
+ }
70
+ button:hover:not(:disabled) { border-color:var(--spark); color:var(--spark-ink); }
71
+ button:active:not(:disabled) { transform:scale(.97); }
72
+ button:disabled { opacity:.4; cursor:not-allowed; }
73
+ button.primary { background:var(--spark); color:#000; border-color:var(--spark); font-weight:700; }
74
+ button.primary:hover:not(:disabled) { color:#000; filter:brightness(1.06); }
75
+
76
+ label { display:flex; flex-direction:column; gap:4px; font-size:12px; color:var(--muted); }
77
+ input, textarea {
78
+ font-family:inherit; font-size:14px; width:100%;
79
+ background:var(--bg); color:var(--text);
80
+ border:1px solid var(--border-strong); border-radius:8px; padding:9px 12px;
81
+ }
82
+ input:focus, textarea:focus { outline:none; border-color:var(--spark); }
83
+ </style>
11
84
  </head>
12
85
  <body>
13
86
  <!-- Each placeholder is replaced by the component file it names. -->
@@ -9,9 +9,11 @@
9
9
  "preview": "vite preview"
10
10
  },
11
11
  "dependencies": {
12
- "spark-html": "latest"
12
+ "spark-html": "latest",
13
+ "spark-html-theme": "latest"
13
14
  },
14
15
  "devDependencies": {
16
+ "spark-prerender": "latest",
15
17
  "vite": "^5.0.0"
16
18
  }
17
19
  }
@@ -1,41 +1,70 @@
1
- <div import="components/welcome"></div>
1
+ <main class="page">
2
+ <div import="components/hero"></div>
3
+
4
+ <section class="demos">
5
+ <h2 class="section-title">Explore <span class="grad">Spark</span></h2>
6
+ <p class="section-desc">
7
+ Every demo is a real component — open <code>public/components/</code> to see how it works.
8
+ </p>
9
+
10
+ <div class="grid">
11
+ <div import="components/demo-todo"></div>
12
+ <div import="components/demo-props"></div>
13
+ </div>
14
+
15
+ <div import="components/demo-await"></div>
16
+ </section>
17
+
18
+ <footer class="foot">
19
+ Edit any file in <code>public/components/</code> and save — the page updates
20
+ instantly. Built with
21
+ <a href="https://github.com/wilkinnovo/spark" target="_blank" rel="noopener">Spark</a>.
22
+ </footer>
23
+ </main>
2
24
 
3
25
  <style>
4
- /* Resets and page-level rules use :global to escape component scoping. */
5
- :global(*),
6
- :global(*::before),
7
- :global(*::after) {
8
- box-sizing: border-box;
9
- margin: 0;
10
- padding: 0;
11
- }
12
- :global(:root) {
13
- --bg: #0c0c11;
14
- --surface: #14141c;
15
- --surface-2: #1b1b26;
16
- --border: #26263a;
17
- --text: #e8e6f0;
18
- --muted: #8b89a0;
19
- --accent: #8b7cff;
20
- --accent-dim: #2a2545;
21
- --spark: #ffd24a;
22
- --radius: 14px;
23
- --mono: ui-monospace, "SF Mono", "Cascadia Code", Menlo, Consolas, monospace;
24
- }
25
- :global(body) {
26
- min-height: 100vh;
27
- font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
28
- color: var(--text);
29
- line-height: 1.6;
30
- background:
31
- radial-gradient(ellipse 70% 50% at 50% -10%, rgba(139, 124, 255, 0.16), transparent),
32
- linear-gradient(rgba(139, 124, 255, 0.035) 1px, transparent 1px),
33
- linear-gradient(90deg, rgba(139, 124, 255, 0.035) 1px, transparent 1px),
34
- var(--bg);
35
- background-size: 100% 100%, 44px 44px, 44px 44px, 100% 100%;
36
- }
37
- :global(::selection) {
38
- background: var(--accent);
39
- color: #0c0c11;
26
+ .page {
27
+ max-width: 960px;
28
+ margin: 0 auto;
29
+ padding: 56px 24px 80px;
30
+ display: flex;
31
+ flex-direction: column;
32
+ gap: 48px;
33
+ }
34
+ .demos {
35
+ display: flex;
36
+ flex-direction: column;
37
+ gap: 20px;
38
+ }
39
+ .section-title {
40
+ font-size: 22px;
41
+ font-weight: 700;
42
+ letter-spacing: -.02em;
43
+ }
44
+ .section-desc {
45
+ font-size: 13px;
46
+ color: var(--muted);
47
+ margin-top: -12px;
48
+ }
49
+ .grid {
50
+ display: grid;
51
+ grid-template-columns: 1fr 1fr;
52
+ gap: 18px;
53
+ align-items: start;
54
+ }
55
+ .foot {
56
+ text-align: center;
57
+ font-size: 13px;
58
+ color: var(--muted);
59
+ margin-top: 8px;
60
+ }
61
+ .foot code { font-size: 12px; padding: 2px 7px; }
62
+ .grad {
63
+ background: linear-gradient(110deg, var(--text), var(--spark));
64
+ -webkit-background-clip: text; background-clip: text; color: transparent;
65
+ }
66
+ @media (max-width: 700px) {
67
+ .grid { grid-template-columns: 1fr; }
68
+ .page { padding-top: 36px; gap: 36px; }
40
69
  }
41
70
  </style>
@@ -0,0 +1,79 @@
1
+ <section class="card">
2
+ <header class="card-header">
3
+ <h2>Async Data <span class="tag">template await + onMount</span></h2>
4
+ </header>
5
+ <p class="hint">
6
+ Declarative loading states with <code>once(fetch())</code> — fires once on mount,
7
+ shows pending, resolved, or error automatically.
8
+ </p>
9
+
10
+ <template await="once(fetchItems())">
11
+ <p class="state pending">
12
+ <span class="spinner"></span> Loading items…
13
+ </p>
14
+ <template then>
15
+ <div class="items">
16
+ <template each="item in await">
17
+ <div class="item-row">
18
+ <span class="id">#{item.id}</span>
19
+ <span>{item.title}</span>
20
+ </div>
21
+ </template>
22
+ </div>
23
+ <p class="state success">Loaded {await.length} items.</p>
24
+ </template>
25
+ <template catch>
26
+ <p class="state error">⚠️ {await.message}</p>
27
+ </template>
28
+ </template>
29
+ </section>
30
+
31
+ <script>
32
+ let cache = null;
33
+
34
+ async function fetchItems() {
35
+ if (cache) return cache;
36
+ await new Promise(r => setTimeout(r, 1500));
37
+ cache = [
38
+ { id: 1, title: 'Components are just .html files' },
39
+ { id: 2, title: 'State is a variable' },
40
+ { id: 3, title: 'Reactivity is an assignment' },
41
+ { id: 4, title: 'Stores share state across components' },
42
+ { id: 5, title: 'The DOM patches itself' },
43
+ ];
44
+ return cache;
45
+ }
46
+
47
+ onMount(() => {
48
+ console.log('[spark] demo-await mounted');
49
+ });
50
+ </script>
51
+
52
+ <style>
53
+ .card { background:var(--surface); border:1px solid var(--border); border-radius:var(--radius); padding:20px; }
54
+ .card-header { margin-bottom:2px; }
55
+ .card-header h2 { font-size:15px; font-weight:700; display:flex; align-items:center; gap:8px; }
56
+ .tag { font-size:10px; font-weight:500; color:var(--spark-ink); border:1px solid var(--border-strong); padding:1px 7px; border-radius:999px; white-space:nowrap; }
57
+ .hint { font-size:12px; color:var(--muted); margin-bottom:14px; line-height:1.5; }
58
+ .hint code { font-size:11px; }
59
+
60
+ .state { font-size:13px; display:flex; align-items:center; gap:10px; padding:16px 0; margin:0; }
61
+ .pending { color:var(--muted); }
62
+ .success { color:var(--muted); margin-top:10px; }
63
+ .error { color:var(--danger); }
64
+
65
+ .spinner {
66
+ display:inline-block; width:14px; height:14px; border-radius:50%;
67
+ border:2px solid var(--border-strong); border-top-color:var(--spark);
68
+ animation:spin .6s linear infinite;
69
+ }
70
+ @keyframes spin { to { transform:rotate(360deg); } }
71
+
72
+ .items { display:flex; flex-direction:column; gap:5px; }
73
+ .item-row {
74
+ display:flex; align-items:center; gap:12px;
75
+ padding:7px 10px; border-radius:6px;
76
+ background:var(--surface-2); font-size:13px;
77
+ }
78
+ .id { color:var(--muted-dim); font-size:11px; min-width:22px; font-variant-numeric:tabular-nums; }
79
+ </style>
@@ -0,0 +1,32 @@
1
+ <section class="card">
2
+ <header class="card-header">
3
+ <h2>Slots + Props <span class="tag">export let + slot</span></h2>
4
+ </header>
5
+ <p class="hint">Components receive content through slots and data through props — both shown here with reusable cards.</p>
6
+
7
+ <div class="feature-grid">
8
+ <div import="components/feature-card" title="Reactive" description="Assign a variable and the DOM updates — that's the whole model." color="#ffd24a">
9
+ <span slot="icon">⚡</span>
10
+ </div>
11
+ <div import="components/feature-card" title="Zero Build" description="No compiler, no bundler. Components are fetched and booted live." color="#6ee7b7">
12
+ <span slot="icon">📄</span>
13
+ </div>
14
+ <div import="components/feature-card" title="Small Runtime" description="~10 KB gzip with zero dependencies. Ships what you need." color="#60a5fa">
15
+ <span slot="icon">📦</span>
16
+ </div>
17
+ <div import="components/feature-card" title="Scoped Styles" description="CSS is scoped per component automatically. :global() when you need out." color="#f472b6">
18
+ <span slot="icon">🎨</span>
19
+ </div>
20
+ </div>
21
+ </section>
22
+
23
+ <style>
24
+ .card { background:var(--surface); border:1px solid var(--border); border-radius:var(--radius); padding:20px; }
25
+ .card-header { margin-bottom:2px; }
26
+ .card-header h2 { font-size:15px; font-weight:700; display:flex; align-items:center; gap:8px; }
27
+ .tag { font-size:10px; font-weight:500; color:var(--spark-ink); border:1px solid var(--border-strong); padding:1px 7px; border-radius:999px; white-space:nowrap; }
28
+ .hint { font-size:12px; color:var(--muted); margin-bottom:14px; line-height:1.5; }
29
+
30
+ .feature-grid { display:grid; grid-template-columns:1fr 1fr; gap:10px; }
31
+ @media (max-width:500px) { .feature-grid { grid-template-columns:1fr; } }
32
+ </style>
@@ -0,0 +1,83 @@
1
+ <section class="card">
2
+ <header class="card-header">
3
+ <h2>Todos <span class="tag">bind:value + each + $:</span></h2>
4
+ <span class="badge">{doneCount}/{todos.length}</span>
5
+ </header>
6
+ <p class="hint">Two-way binding, keyed reconciliation, and reactive statements in action.</p>
7
+
8
+ <div class="add-row">
9
+ <input bind:value="draft" placeholder="What needs doing?" />
10
+ <button class="primary" :disabled="!draft.trim()" onclick={add}>Add</button>
11
+ </div>
12
+
13
+ <div class="list" :hidden="!todos.length">
14
+ <template each="todo in todos" key="todo.id">
15
+ <div class="item" :class="todo.done ? 'done' : ''">
16
+ <label class="check">
17
+ <input type="checkbox" bind:checked="todo.done" />
18
+ </label>
19
+ <span class="text">{todo.text}</span>
20
+ <button class="ghost" onclick={remove(todo.id)} title="Delete">✕</button>
21
+ </div>
22
+ </template>
23
+ </div>
24
+
25
+ <p class="empty-state" :hidden="todos.length > 0">Nothing yet. Add a todo above.</p>
26
+ </section>
27
+
28
+ <script>
29
+ let draft = '';
30
+ let todos = [
31
+ { id: 1, text: 'Learn Spark', done: true },
32
+ { id: 2, text: 'Build something', done: false },
33
+ { id: 3, text: 'Ship it', done: false },
34
+ ];
35
+ let nextId = 4;
36
+
37
+ $: doneCount = todos.filter(t => t.done).length;
38
+
39
+ function add() {
40
+ if (!draft.trim()) return;
41
+ todos = [...todos, { id: nextId++, text: draft.trim(), done: false }];
42
+ draft = '';
43
+ }
44
+
45
+ function remove(id) {
46
+ todos = todos.filter(t => t.id !== id);
47
+ }
48
+ </script>
49
+
50
+ <style>
51
+ .card { background:var(--surface); border:1px solid var(--border); border-radius:var(--radius); padding:20px; }
52
+ .card-header { display:flex; align-items:center; justify-content:space-between; margin-bottom:2px; }
53
+ .card-header h2 { font-size:15px; font-weight:700; display:flex; align-items:center; gap:8px; }
54
+ .badge { font-size:12px; color:var(--muted); font-variant-numeric:tabular-nums; }
55
+ .tag { font-size:10px; font-weight:500; color:var(--spark-ink); border:1px solid var(--border-strong); padding:1px 7px; border-radius:999px; white-space:nowrap; }
56
+ .hint { font-size:12px; color:var(--muted); margin-bottom:14px; line-height:1.5; }
57
+
58
+ .add-row { display:flex; gap:8px; margin-bottom:14px; }
59
+ .add-row input { flex:1; }
60
+
61
+ .list { display:flex; flex-direction:column; gap:6px; }
62
+ .item {
63
+ display:flex; align-items:center; gap:10px;
64
+ padding:8px 10px; border-radius:8px;
65
+ background:var(--surface-2); border:1px solid var(--border);
66
+ transition:opacity .15s;
67
+ }
68
+ .item.done { opacity:.5; }
69
+ .text { flex:1; font-size:13px; }
70
+ .item.done .text { text-decoration:line-through; }
71
+
72
+ .check { display:flex; align-items:center; }
73
+ .check input { width:16px; height:16px; cursor:pointer; }
74
+
75
+ .ghost {
76
+ background:none; border:none; cursor:pointer;
77
+ color:var(--muted); font-size:14px; padding:2px 6px; border-radius:4px;
78
+ transition:color .12s, background .12s;
79
+ }
80
+ .ghost:hover { color:var(--danger); background:rgba(255,107,107,.1); }
81
+
82
+ .empty-state { font-size:12.5px; color:var(--muted-dim); text-align:center; padding:24px 0; }
83
+ </style>
@@ -0,0 +1,30 @@
1
+ <article class="fcard" :style="'--accent:' + color">
2
+ <div class="fcard-icon"><slot name="icon">✦</slot></div>
3
+ <div class="fcard-body">
4
+ <h3>{title}</h3>
5
+ <p>{description}</p>
6
+ </div>
7
+ </article>
8
+
9
+ <script>
10
+ export let title = 'Feature';
11
+ export let description = '';
12
+ export let color = 'var(--spark)';
13
+ </script>
14
+
15
+ <style>
16
+ .fcard {
17
+ background:var(--surface-2); border:1px solid var(--border);
18
+ border-radius:10px; padding:16px;
19
+ display:flex; gap:14px; align-items:flex-start;
20
+ border-left:3px solid var(--accent, var(--spark));
21
+ }
22
+ .fcard-icon {
23
+ font-size:22px; line-height:1; flex-shrink:0;
24
+ width:36px; height:36px; display:flex; align-items:center; justify-content:center;
25
+ background:var(--bg); border-radius:8px;
26
+ }
27
+ .fcard-body { flex:1; min-width:0; }
28
+ .fcard-body h3 { font-size:13px; font-weight:600; margin-bottom:3px; }
29
+ .fcard-body p { font-size:12px; color:var(--muted); line-height:1.5; }
30
+ </style>
@@ -0,0 +1,93 @@
1
+ <header class="hero">
2
+ <div class="top">
3
+ <div class="brand">
4
+ <span class="bolt" :class="igniting ? 'bolt lit' : 'bolt'" onclick="{ignite}" title="Strike the bolt">⚡</span>
5
+ <span class="name">Spark App</span>
6
+ </div>
7
+ <!-- spark-html-theme: one store, toggles light/dark and persists it. -->
8
+ <button class="theme" onclick="{theme.toggle}" title="Toggle theme">
9
+ {theme.resolved === 'dark' ? '☾' : '☀'}
10
+ </button>
11
+ </div>
12
+
13
+ <h1>HTML that <span class="grad">reacts</span>.</h1>
14
+ <p class="tagline">
15
+ Single-file reactive components — no compiler, no virtual DOM, no build step.
16
+ Everything below is live the moment the page loads.
17
+ </p>
18
+
19
+ <!-- Live proof: state is just a variable; $: recomputes on change. -->
20
+ <div class="counter">
21
+ <button class="round" onclick="{count = Math.max(0, count - 1)}" :disabled="count <= 0" aria-label="decrement">–</button>
22
+ <div class="readout">
23
+ <span class="num">{count}</span>
24
+ <span class="sub">doubled is {doubled} · {mood}</span>
25
+ </div>
26
+ <button class="round plus" onclick="{count++}" aria-label="increment">+</button>
27
+ </div>
28
+
29
+ <p class="store-line">
30
+ You've struck the bolt <strong>{app.sparks}</strong>
31
+ time{app.sparks === 1 ? '' : 's'} — that lives in a shared store.
32
+ </p>
33
+ </header>
34
+
35
+ <script>
36
+ // local reactive state — assign to re-patch
37
+ let count = 0;
38
+ let igniting = false;
39
+
40
+ // derived values — recompute automatically when count changes
41
+ $: doubled = count * 2;
42
+ $: mood = count === 0 ? 'a calm start' : count < 5 ? 'warming up' : 'on fire';
43
+
44
+ // shared stores (seeded in main.js / created by theme())
45
+ const app = useStore('app');
46
+ const theme = useStore('theme');
47
+
48
+ function ignite() {
49
+ app.sparks++; // updates every subscriber
50
+ igniting = true;
51
+ setTimeout(() => { igniting = false; }, 450);
52
+ }
53
+ </script>
54
+
55
+ <style>
56
+ .hero { display: flex; flex-direction: column; align-items: center; text-align: center; gap: 16px; }
57
+ .top { width: 100%; display: flex; align-items: center; justify-content: space-between; }
58
+ .brand { display: flex; align-items: center; gap: 10px; }
59
+ .name { font-size: 13px; font-weight: 600; color: var(--muted); letter-spacing: .02em; }
60
+ .bolt {
61
+ font-size: 26px; line-height: 1; cursor: pointer; user-select: none;
62
+ filter: drop-shadow(0 0 14px rgba(255, 210, 74, .45));
63
+ transition: transform .2s ease, filter .2s ease;
64
+ }
65
+ .bolt:hover { transform: translateY(-1px) scale(1.06); }
66
+ .bolt.lit { animation: zap .45s ease; }
67
+ @keyframes zap {
68
+ 0% { transform: scale(1); filter: drop-shadow(0 0 14px rgba(255, 210, 74, .45)); }
69
+ 40% { transform: scale(1.35) rotate(-8deg); filter: drop-shadow(0 0 34px rgba(255, 210, 74, .95)); }
70
+ 100% { transform: scale(1); filter: drop-shadow(0 0 14px rgba(255, 210, 74, .45)); }
71
+ }
72
+ .theme {
73
+ width: 34px; height: 34px; padding: 0; border-radius: 8px; font-size: 15px;
74
+ }
75
+
76
+ h1 { font-size: clamp(32px, 6vw, 48px); font-weight: 800; letter-spacing: -.03em; margin-top: 14px; }
77
+ .grad {
78
+ background: linear-gradient(110deg, var(--text), var(--spark));
79
+ -webkit-background-clip: text; background-clip: text; color: transparent;
80
+ }
81
+ .tagline { max-width: 480px; color: var(--muted); font-size: 14px; }
82
+
83
+ .counter { display: flex; align-items: center; justify-content: center; gap: 22px; margin-top: 8px; }
84
+ .round {
85
+ width: 44px; height: 44px; border-radius: 50%; font-size: 22px; padding: 0;
86
+ }
87
+ .round.plus { background: var(--surface-2); }
88
+ .readout { min-width: 130px; }
89
+ .num { display: block; font-size: 42px; font-weight: 800; font-variant-numeric: tabular-nums; line-height: 1.1; }
90
+ .sub { font-size: 11px; color: var(--muted); }
91
+ .store-line { font-size: 13px; color: var(--muted); }
92
+ .store-line strong { color: var(--spark-ink); }
93
+ </style>
@@ -1,8 +1,11 @@
1
1
  import { mount, store } from 'spark-html';
2
+ import { theme } from 'spark-html-theme';
2
3
 
3
- // Shared, reactive state. Any component can subscribe with useStore('app').
4
- // Assigning a property re-patches every subscriber — that's the whole model.
4
+ // Shared stores connect components without providers or prop drilling.
5
5
  store('app', { sparks: 0 });
6
6
 
7
+ // One-line dark/light/system theming (the ⚡ logo toggles it).
8
+ theme();
9
+
7
10
  // Resolve every <div import="..."> placeholder and boot the components.
8
11
  mount();
@@ -1,10 +1,19 @@
1
1
  import { defineConfig } from 'vite';
2
2
  import spark from 'spark-html/vite';
3
+ import prerender from 'spark-prerender/vite';
3
4
 
4
5
  // Spark needs no build step — Vite is just a convenient dev server and
5
6
  // bundler. The plugin serves component fragments raw and full-reloads
6
7
  // when one changes. Components live in public/ so they ship verbatim to
7
8
  // the production build too.
9
+ //
10
+ // `prerender()` makes `npm run build` SEO-friendly: it runs your app at
11
+ // build time and writes fully-rendered HTML into dist/ (crawlers and AI
12
+ // tools read real content; the browser still hydrates over it). Remove it
13
+ // if you don't need SEO. List every page you ship in `pages`.
8
14
  export default defineConfig({
9
- plugins: [spark()],
15
+ plugins: [
16
+ spark(),
17
+ prerender({ pages: ['index.html'] }),
18
+ ],
10
19
  });
@@ -1,201 +0,0 @@
1
- <main class="wrap">
2
- <header class="hero">
3
- <div class="bolt" onclick={ignite} :class="igniting ? 'bolt lit' : 'bolt'" title="Click me">⚡</div>
4
-
5
- <h1>Welcome to <span class="grad">Spark</span></h1>
6
- <p class="tagline">
7
- Single-file HTML components with built-in reactivity.
8
- No compiler, no virtual DOM, no build step.
9
- </p>
10
-
11
- <div class="badges">
12
- <span class="badge">spark-html</span>
13
- <span class="badge">+ vite</span>
14
- <span class="badge ready">⚡ ready</span>
15
- </div>
16
- </header>
17
-
18
- <!-- Live proof that reactivity works the moment you load the page. -->
19
- <section class="demo">
20
- <p class="demo-label">This counter is live — state is just a variable</p>
21
-
22
- <div class="counter">
23
- <button class="round" onclick={dec} :disabled="count <= 0" aria-label="decrement">–</button>
24
- <div class="readout">
25
- <span class="num">{count}</span>
26
- <span class="doubled">doubled is {doubled} · {mood}</span>
27
- </div>
28
- <button class="round plus" onclick={inc} aria-label="increment">+</button>
29
- </div>
30
-
31
- <p class="store-line">
32
- You've struck the bolt <strong>{app.sparks}</strong>
33
- time{app.sparks === 1 ? '' : 's'} — that value lives in a shared store.
34
- </p>
35
- </section>
36
-
37
- <!-- Next steps -->
38
- <section class="next">
39
- <template each="card in cards">
40
- <a class="card" href="{card.href}" target="_blank" rel="noopener">
41
- <span class="card-ico">{card.ico}</span>
42
- <span class="card-title">{card.title}</span>
43
- <span class="card-body">{card.body}</span>
44
- </a>
45
- </template>
46
- </section>
47
-
48
- <footer class="foot">
49
- Edit <code>public/components/welcome.html</code> and save — the page reloads instantly.
50
- </footer>
51
- </main>
52
-
53
- <script>
54
- // ── local reactive state — assign to re-patch the DOM ──────────────
55
- let count = 0;
56
- let igniting = false;
57
-
58
- // ── derived values — re-run automatically on change ────────────────
59
- $: doubled = count * 2;
60
- $: mood = count === 0 ? 'a calm start' : count < 5 ? 'warming up' : 'on fire';
61
-
62
- // ── shared store, seeded in main.js ────────────────────────────────
63
- const app = useStore('app');
64
-
65
- function inc() { count = count + 1; }
66
- function dec() { count = Math.max(0, count - 1); }
67
-
68
- function ignite() {
69
- app.sparks = app.sparks + 1; // updates every subscriber
70
- igniting = true;
71
- setTimeout(() => { igniting = false; }, 450);
72
- }
73
-
74
- const cards = [
75
- { ico: '📖', title: 'Documentation', body: 'Template syntax, props, stores & the API.',
76
- href: 'https://github.com/wilkinnovo/spark#readme' },
77
- { ico: '🧩', title: 'Components', body: 'Plain .html files in public/components.',
78
- href: 'https://github.com/wilkinnovo/spark' },
79
- { ico: '⚡', title: 'How it works', body: 'No virtual DOM — assignments patch the real one.',
80
- href: 'https://github.com/wilkinnovo/spark#readme' },
81
- ];
82
- </script>
83
-
84
- <style>
85
- .wrap {
86
- max-width: 720px;
87
- margin: 0 auto;
88
- padding: 88px 24px 64px;
89
- display: flex;
90
- flex-direction: column;
91
- gap: 44px;
92
- }
93
-
94
- /* hero */
95
- .hero { text-align: center; display: flex; flex-direction: column; align-items: center; gap: 18px; }
96
- .bolt {
97
- font-size: 72px;
98
- line-height: 1;
99
- cursor: pointer;
100
- user-select: none;
101
- filter: drop-shadow(0 0 22px rgba(255, 210, 74, 0.45));
102
- transition: transform 0.25s ease, filter 0.25s ease;
103
- }
104
- .bolt:hover { transform: translateY(-2px) scale(1.04); }
105
- .bolt.lit { animation: zap 0.45s ease; }
106
- @keyframes zap {
107
- 0% { transform: scale(1); filter: drop-shadow(0 0 22px rgba(255, 210, 74, 0.45)); }
108
- 40% { transform: scale(1.3) rotate(-8deg); filter: drop-shadow(0 0 40px rgba(255, 210, 74, 0.95)); }
109
- 100% { transform: scale(1); filter: drop-shadow(0 0 22px rgba(255, 210, 74, 0.45)); }
110
- }
111
- h1 { font-size: clamp(34px, 7vw, 52px); font-weight: 800; letter-spacing: -0.02em; }
112
- .grad {
113
- background: linear-gradient(110deg, var(--accent), var(--spark));
114
- -webkit-background-clip: text;
115
- background-clip: text;
116
- color: transparent;
117
- }
118
- .tagline { max-width: 460px; color: var(--muted); font-size: 16px; }
119
- .badges { display: flex; gap: 8px; flex-wrap: wrap; justify-content: center; }
120
- .badge {
121
- font-family: var(--mono);
122
- font-size: 12px;
123
- padding: 4px 11px;
124
- border-radius: 999px;
125
- border: 1px solid var(--border);
126
- background: var(--surface);
127
- color: var(--muted);
128
- }
129
- .badge.ready { color: var(--spark); border-color: rgba(255, 210, 74, 0.4); }
130
-
131
- /* demo */
132
- .demo {
133
- background: var(--surface);
134
- border: 1px solid var(--border);
135
- border-radius: var(--radius);
136
- padding: 26px;
137
- text-align: center;
138
- }
139
- .demo-label {
140
- font-family: var(--mono);
141
- font-size: 11px;
142
- text-transform: uppercase;
143
- letter-spacing: 0.14em;
144
- color: var(--muted);
145
- margin-bottom: 18px;
146
- }
147
- .counter { display: flex; align-items: center; justify-content: center; gap: 22px; }
148
- .round {
149
- width: 46px; height: 46px;
150
- border-radius: 50%;
151
- border: 1px solid var(--border);
152
- background: var(--surface-2);
153
- color: var(--text);
154
- font-size: 22px;
155
- cursor: pointer;
156
- transition: border-color 0.15s, transform 0.1s;
157
- }
158
- .round:hover:not(:disabled) { border-color: var(--accent); }
159
- .round:active:not(:disabled) { transform: scale(0.93); }
160
- .round:disabled { opacity: 0.35; cursor: not-allowed; }
161
- .round.plus { background: var(--accent-dim); border-color: var(--accent); color: var(--accent); }
162
- .readout { min-width: 120px; }
163
- .num { display: block; font-size: 46px; font-weight: 800; font-variant-numeric: tabular-nums; }
164
- .doubled { font-size: 12px; color: var(--muted); font-family: var(--mono); }
165
- .store-line { margin-top: 20px; font-size: 14px; color: var(--muted); }
166
- .store-line strong { color: var(--spark); }
167
-
168
- /* next steps */
169
- .next { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; }
170
- .card {
171
- display: flex;
172
- flex-direction: column;
173
- gap: 6px;
174
- padding: 18px;
175
- border-radius: var(--radius);
176
- border: 1px solid var(--border);
177
- background: var(--surface);
178
- text-decoration: none;
179
- color: var(--text);
180
- transition: border-color 0.15s, transform 0.15s;
181
- }
182
- .card:hover { border-color: var(--accent); transform: translateY(-2px); }
183
- .card-ico { font-size: 22px; }
184
- .card-title { font-weight: 700; font-size: 14px; }
185
- .card-body { font-size: 12.5px; color: var(--muted); line-height: 1.5; }
186
-
187
- .foot { text-align: center; font-size: 13px; color: var(--muted); }
188
- .foot code {
189
- font-family: var(--mono);
190
- background: var(--surface);
191
- border: 1px solid var(--border);
192
- border-radius: 6px;
193
- padding: 2px 7px;
194
- color: var(--accent);
195
- }
196
-
197
- @media (max-width: 560px) {
198
- .next { grid-template-columns: 1fr; }
199
- .wrap { padding-top: 56px; }
200
- }
201
- </style>