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 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,5 @@
1
+ node_modules
2
+ dist
3
+ *.local
4
+ .DS_Store
5
+ .vite
@@ -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
+ });