create-spark-html-app 0.4.0 → 0.5.1

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
@@ -1,7 +1,7 @@
1
1
  # create-spark-html-app
2
2
 
3
3
  Scaffold a [Spark](https://github.com/wilkinnovo/spark) app in seconds — a Vite
4
- project wired to `spark-html` with a live, reactive **Welcome to Spark** screen.
4
+ project wired to `spark-html` with live, reactive **Spark** components.
5
5
 
6
6
  ## Usage
7
7
 
@@ -25,11 +25,10 @@ Run it with no name to be prompted:
25
25
  npm create spark-html-app@latest
26
26
  ```
27
27
 
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
28
+ The scaffold is a **multi-page SPA** with client-side routing (`spark-html-router`),
29
+ reactive counters, todo lists with two-way binding and keyed reconciliation,
30
+ slot-based composition, async declarative loading states, and shared stores
31
+ with derived values all in the same monospace dark/light design as the
33
32
  [Spark website](https://wilkinnovo.github.io/spark).
34
33
  Everything is plain HTML and JavaScript — no compiler, no virtual DOM, no
35
34
  proprietary file format. Edit a component, save, and the page updates.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "create-spark-html-app",
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.",
3
+ "version": "0.5.1",
4
+ "description": "Scaffold a Vite + spark-html",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "create-spark-html-app": "bin/index.js"
@@ -3,16 +3,19 @@
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.
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
8
 
9
9
  ## Develop
10
10
 
11
11
  ```bash
12
12
  npm install
13
- npm run dev
13
+ npm run dev # dev server with HMR
14
14
  ```
15
15
 
16
+ In dev mode, `spark-html-devtools` adds a debugging overlay — inspect
17
+ component state, stores, and the mounted tree live.
18
+
16
19
  ## Build (SEO-ready)
17
20
 
18
21
  ```bash
@@ -24,17 +27,23 @@ npm run preview # preview the production build locally
24
27
  Vite plugin runs your app at build time and writes fully-rendered HTML into
25
28
  `dist/` — so crawlers and AI tools read real content (headings, text, links),
26
29
  not empty placeholders. The browser still hydrates over it for full
27
- interactivity. Set page metadata as plain component state:
30
+ interactivity.
28
31
 
29
- ```html
30
- <script>
31
- let pageTitle = 'My App — does a thing';
32
- let pageDescription = 'A short, crawlable description of the page.';
33
- </script>
34
- ```
32
+ Per-route `<title>` and `<meta>` tags are set reactively via
33
+ `spark-html-head` in `src/main.js` — no per-component boilerplate.
35
34
 
36
35
  Don't need SEO? Remove the `prerender(...)` plugin from `vite.config.js`.
37
36
 
37
+ ## Architecture
38
+
39
+ Client routing is set up in `src/main.js` — `router()` (from
40
+ `spark-html-router`) replaces `mount()` and discovers your routes from
41
+ `<template route>` blocks in `index.html`. Per-route `<title>` and `<meta>`
42
+ are handled by `head()` (from `spark-html-head`), and `spark-html-devtools`
43
+ provides a live debugging overlay in dev mode.
44
+
45
+ Each route is just an HTML file in `public/components/`.
46
+
38
47
  ## What's inside
39
48
 
40
49
  The scaffold's components in `public/components/` each demonstrate a Spark feature
@@ -42,11 +51,15 @@ The scaffold's components in `public/components/` each demonstrate a Spark featu
42
51
 
43
52
  | Component | Features shown |
44
53
  |---|---|
45
- | `hero.html` | Local state, `$:` reactive statements, stores (`useStore`), theme toggle |
54
+ | `nav.html` | Client routing (active link highlight via `aria-current="page"`), theme toggle via `useStore('theme')` |
55
+ | `hero.html` | Local state, `$:` reactive declarations, shared store (`useStore('app')`) |
56
+ | `home.html` | Page composition — imports `hero` + demo components for the `/` route |
57
+ | `about.html` | Page composition — uses `feature-card` with props and slots for the `/about` route |
46
58
  | `demo-todo.html` | `bind:value`/`bind:checked`, `<template each>` with `key`, `$:` derived counts |
47
59
  | `demo-props.html` | `export let` props, named `<slot>`, component composition |
48
60
  | `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` |
61
+ | `feature-card.html` | Reusable card via `export let` + `<slot>`, used by `about` and `demo-props` |
62
+ | `footer.html` | Static content component, imported by the shell |
50
63
 
51
64
  A component is a `.html` file with optional `<script>` and `<style>`. Top-level
52
65
  variables are reactive state — assigning to one re-patches that component's DOM.
@@ -9,82 +9,198 @@
9
9
  <link rel="preconnect" href="https://fonts.googleapis.com" />
10
10
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
11
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
12
  <style>
23
13
  /* 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; }
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
+ }
28
24
  :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;
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
+ --font: "JetBrains Mono", ui-monospace, monospace;
35
38
  }
36
39
  [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;
40
+ --bg: #fff;
41
+ --surface: #fafafa;
42
+ --surface-2: #f4f4f5;
43
+ --border: #ededed;
44
+ --border-strong: #d4d4d4;
45
+ --text: #1a1a1a;
46
+ --muted: #666;
47
+ --muted-dim: #999;
48
+ --spark: #ffd24a;
49
+ --spark-ink: #9a6a00;
50
+ --danger: #d63b3b;
51
+ }
52
+ html {
53
+ scroll-behavior: smooth;
41
54
  }
42
- html { scroll-behavior:smooth; }
43
55
  body {
44
- font-family:var(--font); background:var(--bg); color:var(--text);
45
- line-height:1.6; -webkit-font-smoothing:antialiased; min-height:100vh;
56
+ font-family: var(--font);
57
+ background: var(--bg);
58
+ color: var(--text);
59
+ line-height: 1.6;
60
+ -webkit-font-smoothing: antialiased;
61
+ min-height: 100vh;
62
+ display: flex;
63
+ flex-direction: column;
64
+ }
65
+ ::selection {
66
+ background: var(--spark);
67
+ color: #000;
68
+ }
69
+ [data-theme="light"] ::selection {
70
+ background: #ffe9a8;
71
+ color: #111;
72
+ }
73
+ a {
74
+ color: var(--spark-ink);
75
+ text-decoration: none;
76
+ }
77
+ [hidden] {
78
+ display: none !important;
79
+ }
80
+ .routes {
81
+ flex: 1;
82
+ width: 100%;
83
+ max-width: 960px;
84
+ margin: 0 auto;
85
+ padding: 0 24px;
46
86
  }
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
87
 
52
88
  /* Shared component system — components lean on these instead of each
53
- redefining a card/button/input. */
89
+ redefining a card/button/input. */
54
90
  .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; }
91
+ background: var(--surface);
92
+ border: 1px solid var(--border);
93
+ border-radius: var(--radius);
94
+ padding: 22px;
95
+ }
96
+ .card h2 {
97
+ font-size: 16px;
98
+ font-weight: 700;
99
+ margin-bottom: 4px;
100
+ letter-spacing: -0.01em;
101
+ display: flex;
102
+ align-items: center;
103
+ gap: 8px;
104
+ flex-wrap: wrap;
105
+ }
106
+ .tag {
107
+ font-size: 10px;
108
+ font-weight: 500;
109
+ color: var(--spark-ink);
110
+ border: 1px solid var(--border-strong);
111
+ padding: 1px 7px;
112
+ border-radius: 999px;
113
+ }
114
+ .hint {
115
+ font-size: 12.5px;
116
+ color: var(--muted);
117
+ margin-bottom: 16px;
118
+ line-height: 1.5;
119
+ }
120
+ code {
121
+ background: var(--surface-2);
122
+ color: var(--spark-ink);
123
+ padding: 1px 6px;
124
+ border-radius: 4px;
125
+ font-size: 12px;
126
+ }
127
+ .row {
128
+ display: flex;
129
+ gap: 8px;
130
+ align-items: center;
131
+ flex-wrap: wrap;
132
+ }
63
133
 
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); }
134
+ button,
135
+ .btn {
136
+ font-family: inherit;
137
+ font-size: 13px;
138
+ cursor: pointer;
139
+ padding: 8px 15px;
140
+ border-radius: 8px;
141
+ border: 1px solid var(--border-strong);
142
+ background: var(--surface-2);
143
+ color: var(--text);
144
+ transition:
145
+ border-color 0.12s,
146
+ background 0.12s,
147
+ color 0.12s,
148
+ transform 0.08s;
149
+ }
150
+ button:hover:not(:disabled) {
151
+ border-color: var(--spark);
152
+ color: var(--spark-ink);
153
+ }
154
+ button:active:not(:disabled) {
155
+ transform: scale(0.97);
156
+ }
157
+ button:disabled {
158
+ opacity: 0.4;
159
+ cursor: not-allowed;
160
+ }
161
+ button.primary {
162
+ background: var(--spark);
163
+ color: #000;
164
+ border-color: var(--spark);
165
+ font-weight: 700;
166
+ }
167
+ button.primary:hover:not(:disabled) {
168
+ color: #000;
169
+ filter: brightness(1.06);
170
+ }
75
171
 
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;
172
+ label {
173
+ display: flex;
174
+ flex-direction: column;
175
+ gap: 4px;
176
+ font-size: 12px;
177
+ color: var(--muted);
178
+ }
179
+ input,
180
+ textarea {
181
+ font-family: inherit;
182
+ font-size: 14px;
183
+ width: 100%;
184
+ background: var(--bg);
185
+ color: var(--text);
186
+ border: 1px solid var(--border-strong);
187
+ border-radius: 8px;
188
+ padding: 9px 12px;
189
+ }
190
+ input:focus,
191
+ textarea:focus {
192
+ outline: none;
193
+ border-color: var(--spark);
81
194
  }
82
- input:focus, textarea:focus { outline:none; border-color:var(--spark); }
83
195
  </style>
84
196
  </head>
85
197
  <body>
86
- <!-- Each placeholder is replaced by the component file it names. -->
87
- <div import="components/app"></div>
198
+ <div import="components/nav"></div>
199
+ <main class="routes">
200
+ <template route="/"><div import="components/home"></div></template>
201
+ <template route="/about"><div import="components/about"></div></template>
202
+ </main>
203
+ <div import="components/footer"></div>
88
204
 
89
205
  <script type="module" src="/src/main.js"></script>
90
206
  </body>
@@ -10,10 +10,13 @@
10
10
  },
11
11
  "dependencies": {
12
12
  "spark-html": "latest",
13
- "spark-html-theme": "latest"
13
+ "spark-html-router": "latest",
14
+ "spark-html-theme": "latest",
15
+ "spark-html-head": "latest"
14
16
  },
15
17
  "devDependencies": {
16
18
  "spark-prerender": "latest",
17
- "vite": "^5.0.0"
19
+ "spark-html-devtools": "latest",
20
+ "vite": "^8.1.0"
18
21
  }
19
22
  }
@@ -0,0 +1,41 @@
1
+ <article class="about">
2
+ <h1>About <span class="grad">Spark</span></h1>
3
+ <p class="about-desc">
4
+ Single-file reactive HTML components. The <code>.html</code> you save is the component
5
+ that runs — no compiler, no virtual DOM, no build step.
6
+ </p>
7
+
8
+ <div class="features">
9
+ <div import="components/feature-card" title="Reactive $: declarations" description="Prefix any variable with $: to create a derived value — it recomputes automatically when its dependencies change. No subscriptions, no selectors." color="#ffd24a">
10
+ <span slot="icon">⚡</span>
11
+ </div>
12
+ <div import="components/feature-card" title="Two-way bindings" description="Use bind:value to sync inputs with state. Type in the Todo demo and watch the list update — live, with no event wiring." color="#7c3aed">
13
+ <span slot="icon">↔</span>
14
+ </div>
15
+ <div import="components/feature-card" title="Shared stores" description="Call useStore('name') in any component to share state without providers, prop drilling, or context. Updates propagate to every subscriber." color="#10b981">
16
+ <span slot="icon">📦</span>
17
+ </div>
18
+ <div import="components/feature-card" title="Props &amp; slots" description="Export a variable to make it a component prop. Use &lt;slot&gt; to project children from the parent. See demo-props for a live example." color="#f59e0b">
19
+ <span slot="icon">🔌</span>
20
+ </div>
21
+ <div import="components/feature-card" title="Client routing" description="Write routes as &lt;template route=&quot;…&quot;&gt; blocks in your HTML and call router(). SPA navigation, nested layouts, dynamic params, and prerender support — all declarative." color="#ef4444">
22
+ <span slot="icon">🗺</span>
23
+ </div>
24
+ <div import="components/feature-card" title="onMount / onDestroy" description="Lifecycle hooks that fire when a component enters or leaves the DOM. Perfect for timers, subscriptions, and third-party library setup." color="#06b6d4">
25
+ <span slot="icon">⏱</span>
26
+ </div>
27
+ </div>
28
+ </article>
29
+
30
+ <style>
31
+ .about { padding: 48px 0 64px; }
32
+ h1 { font-size: clamp(28px, 5vw, 40px); font-weight: 800; letter-spacing: -.03em; margin-bottom: 10px; }
33
+ .grad {
34
+ background: linear-gradient(110deg, var(--text), var(--spark));
35
+ -webkit-background-clip: text; background-clip: text; color: transparent;
36
+ }
37
+ .about-desc { font-size: 14px; color: var(--muted); max-width: 540px; margin-bottom: 32px; }
38
+ .about-desc code { font-size: 12px; }
39
+ .features { display: flex; flex-direction: column; gap: 12px; }
40
+ @media (max-width: 700px) { .about { padding-top: 36px; } }
41
+ </style>
@@ -11,7 +11,7 @@
11
11
  <div import="components/feature-card" title="Zero Build" description="No compiler, no bundler. Components are fetched and booted live." color="#6ee7b7">
12
12
  <span slot="icon">📄</span>
13
13
  </div>
14
- <div import="components/feature-card" title="Small Runtime" description="~10 KB gzip with zero dependencies. Ships what you need." color="#60a5fa">
14
+ <div import="components/feature-card" title="Small Runtime" description="~11 KB gzip with zero dependencies. Ships what you need." color="#60a5fa">
15
15
  <span slot="icon">📦</span>
16
16
  </div>
17
17
  <div import="components/feature-card" title="Scoped Styles" description="CSS is scoped per component automatically. :global() when you need out." color="#f472b6">
@@ -0,0 +1,17 @@
1
+ <footer class="foot">
2
+ Edit any file in <code>public/components/</code> and save — the page updates
3
+ instantly. Built with
4
+ <a href="https://github.com/wilkinnovo/spark" target="_blank" rel="noopener">Spark</a>.
5
+ </footer>
6
+
7
+ <style>
8
+ .foot {
9
+ text-align: center;
10
+ font-size: 13px;
11
+ color: var(--muted);
12
+ padding: 24px;
13
+ border-top: 1px solid var(--border);
14
+ }
15
+ .foot code { font-size: 12px; padding: 2px 7px; }
16
+ .foot a { color: var(--spark-ink); }
17
+ </style>
@@ -1,14 +1,5 @@
1
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>
2
+ <span class="bolt" :class="igniting ? 'bolt lit' : 'bolt'" onclick="{ignite}" title="Strike the bolt">⚡</span>
12
3
 
13
4
  <h1>HTML that <span class="grad">reacts</span>.</h1>
14
5
  <p class="tagline">
@@ -16,7 +7,6 @@
16
7
  Everything below is live the moment the page loads.
17
8
  </p>
18
9
 
19
- <!-- Live proof: state is just a variable; $: recomputes on change. -->
20
10
  <div class="counter">
21
11
  <button class="round" onclick="{count = Math.max(0, count - 1)}" :disabled="count <= 0" aria-label="decrement">–</button>
22
12
  <div class="readout">
@@ -33,32 +23,25 @@
33
23
  </header>
34
24
 
35
25
  <script>
36
- // local reactive state — assign to re-patch
37
26
  let count = 0;
38
27
  let igniting = false;
39
28
 
40
- // derived values — recompute automatically when count changes
41
29
  $: doubled = count * 2;
42
30
  $: mood = count === 0 ? 'a calm start' : count < 5 ? 'warming up' : 'on fire';
43
31
 
44
- // shared stores (seeded in main.js / created by theme())
45
32
  const app = useStore('app');
46
- const theme = useStore('theme');
47
33
 
48
34
  function ignite() {
49
- app.sparks++; // updates every subscriber
35
+ app.sparks++;
50
36
  igniting = true;
51
37
  setTimeout(() => { igniting = false; }, 450);
52
38
  }
53
39
  </script>
54
40
 
55
41
  <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; }
42
+ .hero { display: flex; flex-direction: column; align-items: center; text-align: center; gap: 16px; padding-top: 32px; }
60
43
  .bolt {
61
- font-size: 26px; line-height: 1; cursor: pointer; user-select: none;
44
+ font-size: 32px; line-height: 1; cursor: pointer; user-select: none;
62
45
  filter: drop-shadow(0 0 14px rgba(255, 210, 74, .45));
63
46
  transition: transform .2s ease, filter .2s ease;
64
47
  }
@@ -69,11 +52,8 @@
69
52
  40% { transform: scale(1.35) rotate(-8deg); filter: drop-shadow(0 0 34px rgba(255, 210, 74, .95)); }
70
53
  100% { transform: scale(1); filter: drop-shadow(0 0 14px rgba(255, 210, 74, .45)); }
71
54
  }
72
- .theme {
73
- width: 34px; height: 34px; padding: 0; border-radius: 8px; font-size: 15px;
74
- }
75
55
 
76
- h1 { font-size: clamp(32px, 6vw, 48px); font-weight: 800; letter-spacing: -.03em; margin-top: 14px; }
56
+ h1 { font-size: clamp(32px, 6vw, 48px); font-weight: 800; letter-spacing: -.03em; }
77
57
  .grad {
78
58
  background: linear-gradient(110deg, var(--text), var(--spark));
79
59
  -webkit-background-clip: text; background-clip: text; color: transparent;
@@ -88,6 +68,6 @@
88
68
  .readout { min-width: 130px; }
89
69
  .num { display: block; font-size: 42px; font-weight: 800; font-variant-numeric: tabular-nums; line-height: 1.1; }
90
70
  .sub { font-size: 11px; color: var(--muted); }
91
- .store-line { font-size: 13px; color: var(--muted); }
71
+ .store-line { font-size: 13px; color: var(--muted); margin-top: 4px; }
92
72
  .store-line strong { color: var(--spark-ink); }
93
73
  </style>
@@ -0,0 +1,48 @@
1
+ <div import="components/hero"></div>
2
+
3
+ <section class="demos">
4
+ <h2 class="section-title">Explore <span class="grad">Spark</span></h2>
5
+ <p class="section-desc">
6
+ Every demo is a real component — open <code>public/components/</code> to see how it works.
7
+ </p>
8
+
9
+ <div class="grid">
10
+ <div import="components/demo-todo"></div>
11
+ <div import="components/demo-props"></div>
12
+ </div>
13
+
14
+ <div import="components/demo-await"></div>
15
+ </section>
16
+
17
+ <style>
18
+ .demos {
19
+ display: flex;
20
+ flex-direction: column;
21
+ gap: 20px;
22
+ padding: 48px 0 64px;
23
+ }
24
+ .section-title {
25
+ font-size: 22px;
26
+ font-weight: 700;
27
+ letter-spacing: -.02em;
28
+ }
29
+ .section-desc {
30
+ font-size: 13px;
31
+ color: var(--muted);
32
+ margin-top: -12px;
33
+ }
34
+ .grid {
35
+ display: grid;
36
+ grid-template-columns: 1fr 1fr;
37
+ gap: 18px;
38
+ align-items: start;
39
+ }
40
+ .grad {
41
+ background: linear-gradient(110deg, var(--text), var(--spark));
42
+ -webkit-background-clip: text; background-clip: text; color: transparent;
43
+ }
44
+ @media (max-width: 700px) {
45
+ .grid { grid-template-columns: 1fr; }
46
+ .demos { padding-top: 36px; gap: 36px; }
47
+ }
48
+ </style>
@@ -0,0 +1,83 @@
1
+ <nav class="nav">
2
+ <div class="nav-inner">
3
+ <a class="nav-brand" href="/">
4
+ <span class="bolt">⚡</span>
5
+ <span class="name">Spark</span>
6
+ </a>
7
+ <div class="nav-links">
8
+ <a href="/">Home</a>
9
+ <a href="/about">About</a>
10
+ </div>
11
+ <button class="theme" onclick="{theme.toggle}" title="Toggle theme">
12
+ {theme.resolved === 'dark' ? '☾' : '☀'}
13
+ </button>
14
+ </div>
15
+ </nav>
16
+
17
+ <script>
18
+ const theme = useStore('theme');
19
+ </script>
20
+
21
+ <style>
22
+ .nav {
23
+ border-bottom: 1px solid var(--border);
24
+ background: var(--surface);
25
+ }
26
+ .nav-inner {
27
+ max-width: 960px;
28
+ margin: 0 auto;
29
+ padding: 0 24px;
30
+ display: flex;
31
+ align-items: center;
32
+ height: 56px;
33
+ gap: 24px;
34
+ }
35
+ .nav-brand {
36
+ display: flex;
37
+ align-items: center;
38
+ gap: 8px;
39
+ color: var(--text) !important;
40
+ }
41
+ .bolt {
42
+ font-size: 18px;
43
+ line-height: 1;
44
+ filter: drop-shadow(0 0 10px rgba(255, 210, 74, .35));
45
+ }
46
+ .name {
47
+ font-size: 14px;
48
+ font-weight: 700;
49
+ }
50
+ .nav-links {
51
+ display: flex;
52
+ gap: 4px;
53
+ }
54
+ .nav-links a {
55
+ color: var(--muted) !important;
56
+ padding: 6px 12px;
57
+ border-radius: 6px;
58
+ font-size: 13px;
59
+ transition: color .12s, background .12s;
60
+ }
61
+ .nav-links a:hover {
62
+ color: var(--text) !important;
63
+ background: var(--surface-2);
64
+ }
65
+ .nav-links a[aria-current="page"] {
66
+ color: var(--spark-ink) !important;
67
+ background: var(--surface-2);
68
+ }
69
+ .theme {
70
+ margin-left: auto;
71
+ width: 34px;
72
+ height: 34px;
73
+ padding: 0;
74
+ border-radius: 8px;
75
+ font-size: 15px;
76
+ display: flex;
77
+ align-items: center;
78
+ justify-content: center;
79
+ }
80
+ @media (max-width: 500px) {
81
+ .nav-links a { font-size: 12px; padding: 6px 8px; }
82
+ }
83
+ </style>
@@ -1,11 +1,23 @@
1
- import { mount, store } from 'spark-html';
2
- import { theme } from 'spark-html-theme';
1
+ import { store } from "spark-html";
2
+ import { router } from "spark-html-router";
3
+ import { theme } from "spark-html-theme";
4
+ import { head } from "spark-html-head";
5
+ import { devtools } from "spark-html-devtools";
6
+
7
+ if (import.meta.env?.DEV) devtools(); // dev only
8
+
9
+ head({
10
+ title: { "/": "Home", "/about": "About", "*": "Not found" },
11
+ titleTemplate: (t) => `${t} · My Site`,
12
+ meta: { description: (path) => `The ${path} page` },
13
+ });
3
14
 
4
15
  // Shared stores connect components without providers or prop drilling.
5
- store('app', { sparks: 0 });
16
+ store("app", { sparks: 0 });
6
17
 
7
18
  // One-line dark/light/system theming (the ⚡ logo toggles it).
8
19
  theme();
9
20
 
10
- // Resolve every <div import="..."> placeholder and boot the components.
11
- mount();
21
+ // Client-side router: reads <template route> blocks, intercepts <a> clicks,
22
+ // and manages SPA navigation. Call it once — replaces mount().
23
+ router();
@@ -1,70 +0,0 @@
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>
24
-
25
- <style>
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; }
69
- }
70
- </style>