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 +5 -6
- package/package.json +2 -2
- package/template/README.md +25 -12
- package/template/index.html +175 -59
- package/template/package.json +5 -2
- package/template/public/components/about.html +41 -0
- package/template/public/components/demo-props.html +1 -1
- package/template/public/components/footer.html +17 -0
- package/template/public/components/hero.html +6 -26
- package/template/public/components/home.html +48 -0
- package/template/public/components/nav.html +83 -0
- package/template/src/main.js +17 -5
- package/template/public/components/app.html +0 -70
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
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
"description": "Scaffold a Vite + spark-html
|
|
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"
|
package/template/README.md
CHANGED
|
@@ -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
|
|
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.
|
|
30
|
+
interactivity.
|
|
28
31
|
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
| `
|
|
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.
|
package/template/index.html
CHANGED
|
@@ -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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
*,
|
|
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
|
|
30
|
-
--
|
|
31
|
-
--
|
|
32
|
-
--
|
|
33
|
-
--
|
|
34
|
-
--
|
|
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
|
|
38
|
-
--
|
|
39
|
-
--
|
|
40
|
-
--
|
|
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);
|
|
45
|
-
|
|
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
|
-
|
|
89
|
+
redefining a card/button/input. */
|
|
54
90
|
.card {
|
|
55
|
-
background:var(--surface);
|
|
56
|
-
border
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
.
|
|
61
|
-
|
|
62
|
-
|
|
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,
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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 {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
87
|
-
<
|
|
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>
|
package/template/package.json
CHANGED
|
@@ -10,10 +10,13 @@
|
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"spark-html": "latest",
|
|
13
|
-
"spark-html-
|
|
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
|
-
"
|
|
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 & slots" description="Export a variable to make it a component prop. Use <slot> 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 <template route="…"> 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="~
|
|
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
|
-
<
|
|
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++;
|
|
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:
|
|
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;
|
|
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>
|
package/template/src/main.js
CHANGED
|
@@ -1,11 +1,23 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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(
|
|
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
|
-
//
|
|
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>
|