create-spark-html-app 0.3.2
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 +46 -0
- package/bin/index.js +134 -0
- package/package.json +33 -0
- package/template/README.md +41 -0
- package/template/_gitignore +5 -0
- package/template/index.html +18 -0
- package/template/package.json +17 -0
- package/template/public/components/app.html +41 -0
- package/template/public/components/welcome.html +201 -0
- package/template/src/main.js +8 -0
- package/template/vite.config.js +10 -0
package/README.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# create-spark-html-app
|
|
2
|
+
|
|
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.
|
|
5
|
+
|
|
6
|
+
## Usage
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npm create spark-html-app@latest my-app
|
|
10
|
+
# or
|
|
11
|
+
npx create-spark-html-app my-app
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Then:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
cd my-app
|
|
18
|
+
npm install
|
|
19
|
+
npm run dev
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Run it with no name to be prompted:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm create spark-html-app@latest
|
|
26
|
+
```
|
|
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
|
+
|
|
41
|
+
Everything is plain HTML and JavaScript — no compiler, no virtual DOM, no
|
|
42
|
+
proprietary file format. Edit a component, save, and the page reloads.
|
|
43
|
+
|
|
44
|
+
## License
|
|
45
|
+
|
|
46
|
+
MIT
|
package/bin/index.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* create-spark-html-app
|
|
4
|
+
*
|
|
5
|
+
* npm create spark-html-app@latest my-app
|
|
6
|
+
* npx create-spark-html-app my-app
|
|
7
|
+
*
|
|
8
|
+
* Scaffolds a ready-to-run Vite + spark-html project with a live,
|
|
9
|
+
* reactive "Welcome to Spark" screen. Zero runtime dependencies —
|
|
10
|
+
* just Node built-ins, in keeping with Spark's no-build ethos.
|
|
11
|
+
*/
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
13
|
+
import { dirname, join, resolve, relative, basename } from 'node:path';
|
|
14
|
+
import {
|
|
15
|
+
cpSync,
|
|
16
|
+
existsSync,
|
|
17
|
+
mkdirSync,
|
|
18
|
+
readdirSync,
|
|
19
|
+
readFileSync,
|
|
20
|
+
renameSync,
|
|
21
|
+
writeFileSync,
|
|
22
|
+
} from 'node:fs';
|
|
23
|
+
import { createInterface } from 'node:readline/promises';
|
|
24
|
+
import { stdin, stdout, argv, exit } from 'node:process';
|
|
25
|
+
|
|
26
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
27
|
+
const templateDir = resolve(here, '..', 'template');
|
|
28
|
+
|
|
29
|
+
// ── tiny ANSI palette (no chalk; one less thing to install) ───────────
|
|
30
|
+
const supportsColor = stdout.isTTY && process.env.NO_COLOR === undefined;
|
|
31
|
+
const paint = (code) => (s) =>
|
|
32
|
+
supportsColor ? `\x1b[${code}m${s}\x1b[0m` : String(s);
|
|
33
|
+
const c = {
|
|
34
|
+
spark: paint('38;5;220'), // ⚡ gold
|
|
35
|
+
accent: paint('38;5;141'), // spark purple
|
|
36
|
+
dim: paint('2'),
|
|
37
|
+
bold: paint('1'),
|
|
38
|
+
green: paint('32'),
|
|
39
|
+
red: paint('31'),
|
|
40
|
+
cyan: paint('36'),
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const BOLT = c.spark('⚡');
|
|
44
|
+
|
|
45
|
+
function bail(msg) {
|
|
46
|
+
stdout.write(`\n${c.red('✘')} ${msg}\n\n`);
|
|
47
|
+
exit(1);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// A valid, polite npm package name.
|
|
51
|
+
function sanitizeName(name) {
|
|
52
|
+
return name
|
|
53
|
+
.trim()
|
|
54
|
+
.toLowerCase()
|
|
55
|
+
.replace(/^[._]+/, '')
|
|
56
|
+
.replace(/[^a-z0-9-~]+/g, '-')
|
|
57
|
+
.replace(/^-+|-+$/g, '');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function isEmptyDir(dir) {
|
|
61
|
+
if (!existsSync(dir)) return true;
|
|
62
|
+
const entries = readdirSync(dir).filter((f) => f !== '.git');
|
|
63
|
+
return entries.length === 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function prompt(question, fallback) {
|
|
67
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
68
|
+
try {
|
|
69
|
+
const answer = (await rl.question(question)).trim();
|
|
70
|
+
return answer || fallback;
|
|
71
|
+
} finally {
|
|
72
|
+
rl.close();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function main() {
|
|
77
|
+
stdout.write(`\n${BOLT} ${c.bold('create-spark-html-app')}\n`);
|
|
78
|
+
stdout.write(`${c.dim(' HTML that reacts — no compiler, no virtual DOM.')}\n\n`);
|
|
79
|
+
|
|
80
|
+
// 1 ─ figure out the target directory ────────────────────────────────
|
|
81
|
+
let targetArg = argv[2];
|
|
82
|
+
if (!targetArg) {
|
|
83
|
+
if (!stdin.isTTY) bail('Please pass a project name: create-spark-html-app <name>');
|
|
84
|
+
targetArg = await prompt(
|
|
85
|
+
`${c.accent('?')} Project name: ${c.dim('(my-spark-app)')} `,
|
|
86
|
+
'my-spark-app',
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const targetDir = resolve(process.cwd(), targetArg);
|
|
91
|
+
const projectName = sanitizeName(basename(targetDir)) || 'my-spark-app';
|
|
92
|
+
|
|
93
|
+
// 2 ─ make sure we won't clobber anything ────────────────────────────
|
|
94
|
+
if (!isEmptyDir(targetDir)) {
|
|
95
|
+
if (!stdin.isTTY) bail(`Directory "${targetArg}" already exists and is not empty.`);
|
|
96
|
+
const ok = await prompt(
|
|
97
|
+
`${c.accent('?')} "${targetArg}" is not empty. Continue and overwrite files? ${c.dim('(y/N)')} `,
|
|
98
|
+
'n',
|
|
99
|
+
);
|
|
100
|
+
if (!/^y(es)?$/i.test(ok)) bail('Aborted — nothing was written.');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 3 ─ copy the template ──────────────────────────────────────────────
|
|
104
|
+
mkdirSync(targetDir, { recursive: true });
|
|
105
|
+
cpSync(templateDir, targetDir, { recursive: true });
|
|
106
|
+
|
|
107
|
+
// npm renames/strips dotfiles on publish, so the template ships them
|
|
108
|
+
// with safe underscore prefixes. Restore the real names here.
|
|
109
|
+
const dotfiles = [
|
|
110
|
+
['_gitignore', '.gitignore'],
|
|
111
|
+
['_npmrc', '.npmrc'],
|
|
112
|
+
];
|
|
113
|
+
for (const [from, to] of dotfiles) {
|
|
114
|
+
const src = join(targetDir, from);
|
|
115
|
+
if (existsSync(src)) renameSync(src, join(targetDir, to));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// 4 ─ stamp the project name into package.json ───────────────────────
|
|
119
|
+
const pkgPath = join(targetDir, 'package.json');
|
|
120
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
121
|
+
pkg.name = projectName;
|
|
122
|
+
writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
|
|
123
|
+
|
|
124
|
+
// 5 ─ celebrate + print next steps ───────────────────────────────────
|
|
125
|
+
const rel = relative(process.cwd(), targetDir) || '.';
|
|
126
|
+
stdout.write(`\n${c.green('✔')} Scaffolded ${c.bold(projectName)} in ${c.cyan(rel)}\n\n`);
|
|
127
|
+
stdout.write(`${c.bold('Next steps:')}\n`);
|
|
128
|
+
if (rel !== '.') stdout.write(` ${c.dim('1.')} cd ${rel}\n`);
|
|
129
|
+
stdout.write(` ${c.dim(rel !== '.' ? '2.' : '1.')} npm install\n`);
|
|
130
|
+
stdout.write(` ${c.dim(rel !== '.' ? '3.' : '2.')} npm run dev\n\n`);
|
|
131
|
+
stdout.write(`${BOLT} Then open the dev server and edit ${c.cyan('public/components/welcome.html')}.\n\n`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
main().catch((err) => bail(err?.message || String(err)));
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-spark-html-app",
|
|
3
|
+
"version": "0.3.2",
|
|
4
|
+
"description": "Scaffold a Vite + spark-html app with a live reactive welcome screen.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"create-spark-html-app": "bin/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"template"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=18"
|
|
15
|
+
},
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/wilkinnovo/spark.git",
|
|
19
|
+
"directory": "packages/create-spark-html-app"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"spark",
|
|
23
|
+
"spark-html",
|
|
24
|
+
"create",
|
|
25
|
+
"scaffold",
|
|
26
|
+
"cli",
|
|
27
|
+
"vite",
|
|
28
|
+
"reactive",
|
|
29
|
+
"html",
|
|
30
|
+
"no-build"
|
|
31
|
+
],
|
|
32
|
+
"license": "MIT"
|
|
33
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# ⚡ Spark App
|
|
2
|
+
|
|
3
|
+
A starter built with [spark-html](https://github.com/wilkinnovo/spark) — single-file
|
|
4
|
+
HTML components with built-in reactivity. No compiler, no virtual DOM, no build step.
|
|
5
|
+
|
|
6
|
+
## Develop
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npm install
|
|
10
|
+
npm run dev
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Open the dev server and edit `public/components/welcome.html`. Save, and the
|
|
14
|
+
page reloads instantly.
|
|
15
|
+
|
|
16
|
+
## Build
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm run build # static output → dist/, serve anywhere
|
|
20
|
+
npm run preview # preview the production build locally
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## How it's wired
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
.
|
|
27
|
+
├── index.html ← <div import="components/app"> + boot script
|
|
28
|
+
├── src/main.js ← mount() + a shared store
|
|
29
|
+
├── public/components/ ← your components (plain .html files)
|
|
30
|
+
│ ├── app.html ← theme + shell
|
|
31
|
+
│ └── welcome.html ← the live reactive welcome screen
|
|
32
|
+
└── vite.config.js ← spark-html/vite plugin
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
A component is a `.html` file with optional `<script>` and `<style>`. Top-level
|
|
36
|
+
variables are reactive state — assigning to one re-patches that component's DOM.
|
|
37
|
+
Derive values with `$:`, share state across components with `useStore(name)`,
|
|
38
|
+
and pass props as attributes on the `import` placeholder.
|
|
39
|
+
|
|
40
|
+
See the [full docs](https://github.com/wilkinnovo/spark#readme) for the
|
|
41
|
+
complete template syntax reference.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
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
|
+
/>
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
<!-- Each placeholder is replaced by the component file it names. -->
|
|
14
|
+
<div import="components/app"></div>
|
|
15
|
+
|
|
16
|
+
<script type="module" src="/src/main.js"></script>
|
|
17
|
+
</body>
|
|
18
|
+
</html>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "spark-app",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "0.0.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"build": "vite build",
|
|
9
|
+
"preview": "vite preview"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"spark-html": "^0.12.0"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"vite": "^5.0.0"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
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>
|
|
@@ -0,0 +1,201 @@
|
|
|
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>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { mount, store } from 'spark-html';
|
|
2
|
+
|
|
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 });
|
|
6
|
+
|
|
7
|
+
// Resolve every <div import="..."> placeholder and boot the components.
|
|
8
|
+
mount();
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { defineConfig } from 'vite';
|
|
2
|
+
import spark from 'spark-html/vite';
|
|
3
|
+
|
|
4
|
+
// Spark needs no build step — Vite is just a convenient dev server and
|
|
5
|
+
// bundler. The plugin serves component fragments raw and full-reloads
|
|
6
|
+
// when one changes. Components live in public/ so they ship verbatim to
|
|
7
|
+
// the production build too.
|
|
8
|
+
export default defineConfig({
|
|
9
|
+
plugins: [spark()],
|
|
10
|
+
});
|