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 +7 -15
- package/bin/index.js +15 -7
- package/package.json +2 -2
- package/template/README.md +39 -24
- package/template/index.html +195 -6
- package/template/package.json +5 -2
- package/template/public/components/about.html +41 -0
- package/template/public/components/demo-await.html +79 -0
- package/template/public/components/demo-props.html +32 -0
- package/template/public/components/demo-todo.html +83 -0
- package/template/public/components/feature-card.html +30 -0
- package/template/public/components/footer.html +17 -0
- package/template/public/components/hero.html +73 -0
- package/template/public/components/home.html +48 -0
- package/template/public/components/nav.html +83 -0
- package/template/src/main.js +21 -6
- package/template/public/components/app.html +0 -41
- package/template/public/components/welcome.html +0 -201
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,21 +25,13 @@ 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
|
-
|
|
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
|
|
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
|
|
146
|
-
// be reached
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
pkg
|
|
150
|
-
|
|
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/
|
|
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.
|
|
4
|
-
"description": "Scaffold a Vite + spark-html
|
|
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"
|
package/template/README.md
CHANGED
|
@@ -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
|
-
|
|
14
|
-
|
|
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.
|
|
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
|
|
|
38
|
-
##
|
|
37
|
+
## Architecture
|
|
39
38
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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`
|
|
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.
|
|
56
|
-
|
|
70
|
+
See the [full docs](https://wilkinnovo.github.io/spark/docs) for the complete
|
|
71
|
+
template syntax reference.
|
package/template/index.html
CHANGED
|
@@ -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
|
-
<
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
14
|
-
<
|
|
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>
|
package/template/package.json
CHANGED
|
@@ -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
|
-
"
|
|
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 & 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>
|
|
@@ -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>
|
package/template/src/main.js
CHANGED
|
@@ -1,8 +1,23 @@
|
|
|
1
|
-
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";
|
|
2
6
|
|
|
3
|
-
|
|
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
|
-
|
|
8
|
-
|
|
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>
|