create-spark-html-app 0.3.4 → 0.5.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
@@ -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,21 +25,13 @@ 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
+ 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
32
+ [Spark website](https://wilkinnovo.github.io/spark).
41
33
  Everything is plain HTML and JavaScript — no compiler, no virtual DOM, no
42
- proprietary file format. Edit a component, save, and the page reloads.
34
+ proprietary file format. Edit a component, save, and the page updates.
43
35
 
44
36
  ## License
45
37
 
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.4",
4
- "description": "Scaffold a Vite + spark-html app with a live reactive welcome screen.",
3
+ "version": "0.5.0",
4
+ "description": "Scaffold a Vite + spark-html",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "create-spark-html-app": "bin/index.js"
@@ -3,15 +3,18 @@
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 **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
+
6
9
  ## Develop
7
10
 
8
11
  ```bash
9
12
  npm install
10
- npm run dev
13
+ npm run dev # dev server with HMR
11
14
  ```
12
15
 
13
- Open the dev server and edit `public/components/welcome.html`. Save, and the
14
- page reloads instantly.
16
+ In dev mode, `spark-html-devtools` adds a debugging overlay — inspect
17
+ component state, stores, and the mounted tree live.
15
18
 
16
19
  ## Build (SEO-ready)
17
20
 
@@ -24,33 +27,45 @@ 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
 
38
- ## How it's wired
37
+ ## Architecture
39
38
 
40
- ```
41
- .
42
- ├── index.html <div import="components/app"> + boot script
43
- ├── src/main.js ← mount() + a shared store
44
- ├── public/components/ ← your components (plain .html files)
45
- │ ├── app.html ← theme + shell
46
- │ └── welcome.html ← the live reactive welcome screen
47
- └── vite.config.js ← spark-html/vite + spark-prerender/vite (SEO)
48
- ```
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
+
47
+ ## What's inside
48
+
49
+ The scaffold's components in `public/components/` each demonstrate a Spark feature
50
+ (all using only the published runtime — no experimental APIs):
51
+
52
+ | Component | Features shown |
53
+ |---|---|
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 |
58
+ | `demo-todo.html` | `bind:value`/`bind:checked`, `<template each>` with `key`, `$:` derived counts |
59
+ | `demo-props.html` | `export let` props, named `<slot>`, component composition |
60
+ | `demo-await.html` | `<template await>` with `once()`, `onMount`, loading/then/catch states |
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 |
49
63
 
50
64
  A component is a `.html` file with optional `<script>` and `<style>`. Top-level
51
65
  variables are reactive state — assigning to one re-patches that component's DOM.
52
- Derive values with `$:`, share state across components with `useStore(name)`,
53
- and pass props as attributes on the `import` placeholder.
66
+ Derive values with `$:`, share state across components with `useStore(name)`, use
67
+ `bind:value` for two-way binds, and pass props as attributes on the `import`
68
+ placeholder.
54
69
 
55
- See the [full docs](https://github.com/wilkinnovo/spark#readme) for the
56
- complete template syntax reference.
70
+ See the [full docs](https://wilkinnovo.github.io/spark/docs) for the complete
71
+ template syntax reference.
@@ -4,14 +4,203 @@
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
+ <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
+ --font: "JetBrains Mono", ui-monospace, monospace;
38
+ }
39
+ [data-theme="light"] {
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;
54
+ }
55
+ body {
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;
86
+ }
87
+
88
+ /* Shared component system — components lean on these instead of each
89
+ redefining a card/button/input. */
90
+ .card {
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
+ }
133
+
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
+ }
171
+
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);
194
+ }
195
+ </style>
11
196
  </head>
12
197
  <body>
13
- <!-- Each placeholder is replaced by the component file it names. -->
14
- <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>
15
204
 
16
205
  <script type="module" src="/src/main.js"></script>
17
206
  </body>
@@ -9,10 +9,13 @@
9
9
  "preview": "vite preview"
10
10
  },
11
11
  "dependencies": {
12
- "spark-html": "latest"
12
+ "spark-html": "latest",
13
+ "spark-html-router": "latest",
14
+ "spark-html-theme": "latest"
13
15
  },
14
16
  "devDependencies": {
15
17
  "spark-prerender": "latest",
16
- "vite": "^5.0.0"
18
+ "spark-html-devtools": "latest",
19
+ "vite": "^8.1.0"
17
20
  }
18
21
  }
@@ -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>
@@ -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="~11 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,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>
@@ -0,0 +1,73 @@
1
+ <header class="hero">
2
+ <span class="bolt" :class="igniting ? 'bolt lit' : 'bolt'" onclick="{ignite}" title="Strike the bolt">⚡</span>
3
+
4
+ <h1>HTML that <span class="grad">reacts</span>.</h1>
5
+ <p class="tagline">
6
+ Single-file reactive components — no compiler, no virtual DOM, no build step.
7
+ Everything below is live the moment the page loads.
8
+ </p>
9
+
10
+ <div class="counter">
11
+ <button class="round" onclick="{count = Math.max(0, count - 1)}" :disabled="count <= 0" aria-label="decrement">–</button>
12
+ <div class="readout">
13
+ <span class="num">{count}</span>
14
+ <span class="sub">doubled is {doubled} · {mood}</span>
15
+ </div>
16
+ <button class="round plus" onclick="{count++}" aria-label="increment">+</button>
17
+ </div>
18
+
19
+ <p class="store-line">
20
+ You've struck the bolt <strong>{app.sparks}</strong>
21
+ time{app.sparks === 1 ? '' : 's'} — that lives in a shared store.
22
+ </p>
23
+ </header>
24
+
25
+ <script>
26
+ let count = 0;
27
+ let igniting = false;
28
+
29
+ $: doubled = count * 2;
30
+ $: mood = count === 0 ? 'a calm start' : count < 5 ? 'warming up' : 'on fire';
31
+
32
+ const app = useStore('app');
33
+
34
+ function ignite() {
35
+ app.sparks++;
36
+ igniting = true;
37
+ setTimeout(() => { igniting = false; }, 450);
38
+ }
39
+ </script>
40
+
41
+ <style>
42
+ .hero { display: flex; flex-direction: column; align-items: center; text-align: center; gap: 16px; padding-top: 32px; }
43
+ .bolt {
44
+ font-size: 32px; line-height: 1; cursor: pointer; user-select: none;
45
+ filter: drop-shadow(0 0 14px rgba(255, 210, 74, .45));
46
+ transition: transform .2s ease, filter .2s ease;
47
+ }
48
+ .bolt:hover { transform: translateY(-1px) scale(1.06); }
49
+ .bolt.lit { animation: zap .45s ease; }
50
+ @keyframes zap {
51
+ 0% { transform: scale(1); filter: drop-shadow(0 0 14px rgba(255, 210, 74, .45)); }
52
+ 40% { transform: scale(1.35) rotate(-8deg); filter: drop-shadow(0 0 34px rgba(255, 210, 74, .95)); }
53
+ 100% { transform: scale(1); filter: drop-shadow(0 0 14px rgba(255, 210, 74, .45)); }
54
+ }
55
+
56
+ h1 { font-size: clamp(32px, 6vw, 48px); font-weight: 800; letter-spacing: -.03em; }
57
+ .grad {
58
+ background: linear-gradient(110deg, var(--text), var(--spark));
59
+ -webkit-background-clip: text; background-clip: text; color: transparent;
60
+ }
61
+ .tagline { max-width: 480px; color: var(--muted); font-size: 14px; }
62
+
63
+ .counter { display: flex; align-items: center; justify-content: center; gap: 22px; margin-top: 8px; }
64
+ .round {
65
+ width: 44px; height: 44px; border-radius: 50%; font-size: 22px; padding: 0;
66
+ }
67
+ .round.plus { background: var(--surface-2); }
68
+ .readout { min-width: 130px; }
69
+ .num { display: block; font-size: 42px; font-weight: 800; font-variant-numeric: tabular-nums; line-height: 1.1; }
70
+ .sub { font-size: 11px; color: var(--muted); }
71
+ .store-line { font-size: 13px; color: var(--muted); margin-top: 4px; }
72
+ .store-line strong { color: var(--spark-ink); }
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,8 +1,23 @@
1
- import { mount, store } from 'spark-html';
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";
2
6
 
3
- // Shared, reactive state. Any component can subscribe with useStore('app').
4
- // Assigning a property re-patches every subscriber — that's the whole model.
5
- store('app', { sparks: 0 });
7
+ if (import.meta.env?.DEV) devtools(); // dev only
6
8
 
7
- // Resolve every <div import="..."> placeholder and boot the components.
8
- mount();
9
+ head({
10
+ title: { "/": "Home", "/about": "About", "*": "Not found" },
11
+ titleTemplate: (t) => `${t} · My Site`,
12
+ meta: { description: (path) => `The ${path} page` },
13
+ });
14
+
15
+ // Shared stores connect components without providers or prop drilling.
16
+ store("app", { sparks: 0 });
17
+
18
+ // One-line dark/light/system theming (the ⚡ logo toggles it).
19
+ theme();
20
+
21
+ // Client-side router: reads <template route> blocks, intercepts <a> clicks,
22
+ // and manages SPA navigation. Call it once — replaces mount().
23
+ router();
@@ -1,41 +0,0 @@
1
- <div import="components/welcome"></div>
2
-
3
- <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;
40
- }
41
- </style>
@@ -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>