create-zoijs 0.1.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/CHANGELOG.md +19 -0
- package/LICENSE +21 -0
- package/README.md +76 -0
- package/bin/create-zoijs.js +169 -0
- package/package.json +43 -0
- package/templates/app/README.md +38 -0
- package/templates/app/_gitignore +1 -0
- package/templates/app/index.html +22 -0
- package/templates/app/package.json +12 -0
- package/templates/app/src/app.js +55 -0
- package/templates/app/src/components/Header.js +14 -0
- package/templates/app/src/components/TaskItem.js +18 -0
- package/templates/app/src/style.css +91 -0
- package/templates/basic/README.md +23 -0
- package/templates/basic/_gitignore +1 -0
- package/templates/basic/index.html +22 -0
- package/templates/basic/package.json +12 -0
- package/templates/basic/src/app.js +18 -0
- package/templates/basic/src/style.css +33 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `create-zoijs` are documented here.
|
|
4
|
+
|
|
5
|
+
## 0.1.0 — 2026-06-25
|
|
6
|
+
|
|
7
|
+
Initial release of the Zoijs starter CLI.
|
|
8
|
+
|
|
9
|
+
- `npm create zoijs@latest my-app` — scaffold a new Zoijs app in one command.
|
|
10
|
+
- Uses the provided folder name as the project name; derives `package.json`
|
|
11
|
+
`name` (lowercased) and a readable `index.html` `<title>` from it.
|
|
12
|
+
- Two templates: `app` (default — a small task dashboard demonstrating
|
|
13
|
+
`html` / `mount` / `createState` / `computed` / `each` and parent⇄child
|
|
14
|
+
communication) and `basic` (a minimal counter). Select with `--template`.
|
|
15
|
+
- Validates the project name (npm-safe) and refuses to overwrite a non-empty
|
|
16
|
+
directory.
|
|
17
|
+
- Generated apps are plain HTML/CSS/JS with **no build step** and depend only on
|
|
18
|
+
`@zoijs/core` (served via an import map from `node_modules`).
|
|
19
|
+
- Zero runtime dependencies; the CLI is a single small file.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Zoijs contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# create-zoijs
|
|
4
|
+
|
|
5
|
+
**Create a new [Zoijs](https://zoijs.dev) app with one command.** Plain HTML, CSS, and JavaScript — no build step.
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/create-zoijs)
|
|
8
|
+
[](LICENSE)
|
|
9
|
+
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Usage
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm create zoijs@latest my-app
|
|
18
|
+
cd my-app
|
|
19
|
+
npm install
|
|
20
|
+
npm run dev
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Or with `npx`:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npx create-zoijs my-app
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
> **A note on the name.** npm maps `npm create zoijs` to the package
|
|
30
|
+
> `create-zoijs` (that's npm's convention for `npm create <name>`), which is why
|
|
31
|
+
> this package is `create-zoijs` rather than `@zoijs/create`.
|
|
32
|
+
|
|
33
|
+
## Naming your app
|
|
34
|
+
|
|
35
|
+
The folder name you pass becomes the project name:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npm create zoijs@latest task-manager
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
produces a `package.json` with `"name": "task-manager"` and an `index.html`
|
|
42
|
+
with `<title>Task Manager</title>`. If you don't pass a name, the CLI asks for
|
|
43
|
+
one.
|
|
44
|
+
|
|
45
|
+
## Templates
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npm create zoijs@latest my-app # default: the "app" template
|
|
49
|
+
npm create zoijs@latest my-app --template basic # a minimal counter
|
|
50
|
+
npm create zoijs@latest my-app --template app # a small task dashboard
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
- **`app`** (default) — a small task dashboard showing `html`, `mount`,
|
|
54
|
+
`createState`, `computed`, `each`, and parent⇄child component communication.
|
|
55
|
+
- **`basic`** — a minimal counter, the smallest possible Zoijs app.
|
|
56
|
+
|
|
57
|
+
## What it does
|
|
58
|
+
|
|
59
|
+
- Copies a template, fills in the app name, and prints the next three commands.
|
|
60
|
+
- Validates the name (npm-safe) and won't overwrite a non-empty folder.
|
|
61
|
+
|
|
62
|
+
## What it does **not** do
|
|
63
|
+
|
|
64
|
+
By design, to keep Zoijs simple: no build system, no bundler, no compiler, no
|
|
65
|
+
JSX transform, no code generation, no plugin system, no template DSL, and no
|
|
66
|
+
complex prompts. The generated app is plain files you can read.
|
|
67
|
+
|
|
68
|
+
## You don't need this tool
|
|
69
|
+
|
|
70
|
+
Zoijs works without the CLI — it's only a convenience. You can create an app by
|
|
71
|
+
hand with an import map and a `<script type="module">`, or with
|
|
72
|
+
`npm install @zoijs/core`. See the [Start guide](https://zoijs.dev/start).
|
|
73
|
+
|
|
74
|
+
## License
|
|
75
|
+
|
|
76
|
+
[MIT](LICENSE) © Zoijs contributors
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// create-zoijs — a tiny scaffolder for new Zoijs apps. No build step, no
|
|
3
|
+
// dependencies. It copies a template, fills in the app name, and prints the
|
|
4
|
+
// three commands to get going. That's the whole tool — read it top to bottom.
|
|
5
|
+
//
|
|
6
|
+
// Usage:
|
|
7
|
+
// npm create zoijs@latest my-app (npm maps this to create-zoijs)
|
|
8
|
+
// npx create-zoijs my-app --template basic
|
|
9
|
+
//
|
|
10
|
+
// The pure functions below are exported so they can be unit-tested; main() only
|
|
11
|
+
// runs when this file is executed directly (not when imported).
|
|
12
|
+
|
|
13
|
+
import fs from "node:fs";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
import readline from "node:readline";
|
|
16
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
17
|
+
|
|
18
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
|
|
20
|
+
export const TEMPLATES = ["basic", "app"];
|
|
21
|
+
export const DEFAULT_TEMPLATE = "app";
|
|
22
|
+
const TEMPLATES_DIR = path.join(__dirname, "..", "templates");
|
|
23
|
+
|
|
24
|
+
// ---- argument parsing --------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
/** Parse argv into { name, template }. The first non-flag arg is the app name. */
|
|
27
|
+
export function parseArgs(argv) {
|
|
28
|
+
let name;
|
|
29
|
+
let template = DEFAULT_TEMPLATE;
|
|
30
|
+
for (let i = 0; i < argv.length; i++) {
|
|
31
|
+
const a = argv[i];
|
|
32
|
+
if (a === "--template" || a === "-t") template = argv[++i];
|
|
33
|
+
else if (a.startsWith("--template=")) template = a.slice("--template=".length);
|
|
34
|
+
else if (!a.startsWith("-") && name === undefined) name = a;
|
|
35
|
+
}
|
|
36
|
+
return { name, template };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ---- naming ------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
/** A readable Title Case form of a project name: "task-manager" → "Task Manager". */
|
|
42
|
+
export function toTitle(name) {
|
|
43
|
+
return String(name)
|
|
44
|
+
.replace(/[-_]+/g, " ")
|
|
45
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
46
|
+
.trim()
|
|
47
|
+
.split(/\s+/)
|
|
48
|
+
.filter(Boolean)
|
|
49
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
50
|
+
.join(" ");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Validate a project name. Returns { valid, problems[] }. Keeps it npm-safe. */
|
|
54
|
+
export function validateName(name) {
|
|
55
|
+
const problems = [];
|
|
56
|
+
if (!name || !name.trim()) {
|
|
57
|
+
problems.push("Project name cannot be empty.");
|
|
58
|
+
return { valid: false, problems };
|
|
59
|
+
}
|
|
60
|
+
if (name.trim() !== name) problems.push("Project name cannot have leading or trailing spaces.");
|
|
61
|
+
if (name.length > 214) problems.push("Project name must be 214 characters or fewer.");
|
|
62
|
+
if (/^[._]/.test(name)) problems.push("Project name cannot start with a dot or underscore.");
|
|
63
|
+
if (!/^[a-zA-Z0-9._-]+$/.test(name)) {
|
|
64
|
+
problems.push("Project name may only contain letters, numbers, dashes, dots, and underscores.");
|
|
65
|
+
}
|
|
66
|
+
return { valid: problems.length === 0, problems };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ---- filesystem --------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
/** Throw if the target directory exists and already has files in it. */
|
|
72
|
+
export function ensureTargetAvailable(targetDir) {
|
|
73
|
+
if (fs.existsSync(targetDir) && fs.readdirSync(targetDir).length > 0) {
|
|
74
|
+
throw new Error(`Directory "${path.basename(targetDir)}" already exists and is not empty.`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const TEXT_EXT = new Set([".js", ".html", ".css", ".json", ".md", ".txt", ".svg"]);
|
|
79
|
+
const isText = (file) => file === "_gitignore" || TEXT_EXT.has(path.extname(file));
|
|
80
|
+
|
|
81
|
+
function applyTokens(content, tokens) {
|
|
82
|
+
return content.replaceAll("{{APP_NAME}}", tokens.APP_NAME).replaceAll("{{APP_TITLE}}", tokens.APP_TITLE);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function copyDir(srcDir, destDir, tokens, written) {
|
|
86
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
87
|
+
for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
|
|
88
|
+
const srcPath = path.join(srcDir, entry.name);
|
|
89
|
+
// npm strips a literal ".gitignore" from published packages, so templates
|
|
90
|
+
// ship it as "_gitignore" and we rename it on copy.
|
|
91
|
+
const destName = entry.name === "_gitignore" ? ".gitignore" : entry.name;
|
|
92
|
+
const destPath = path.join(destDir, destName);
|
|
93
|
+
if (entry.isDirectory()) {
|
|
94
|
+
copyDir(srcPath, destPath, tokens, written);
|
|
95
|
+
} else if (isText(entry.name)) {
|
|
96
|
+
fs.writeFileSync(destPath, applyTokens(fs.readFileSync(srcPath, "utf8"), tokens));
|
|
97
|
+
written.push(destPath);
|
|
98
|
+
} else {
|
|
99
|
+
fs.copyFileSync(srcPath, destPath);
|
|
100
|
+
written.push(destPath);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Copy a template into targetDir, substituting the app name. Pure FS work — the
|
|
107
|
+
* caller decides the name/template/dirs, which makes this easy to test.
|
|
108
|
+
*/
|
|
109
|
+
export function scaffold({ name, template = DEFAULT_TEMPLATE, targetDir, templatesDir = TEMPLATES_DIR }) {
|
|
110
|
+
const tplDir = path.join(templatesDir, template);
|
|
111
|
+
if (!TEMPLATES.includes(template) || !fs.existsSync(tplDir)) {
|
|
112
|
+
throw new Error(`Unknown template "${template}". Available templates: ${TEMPLATES.join(", ")}.`);
|
|
113
|
+
}
|
|
114
|
+
const tokens = { APP_NAME: name.toLowerCase(), APP_TITLE: toTitle(name) };
|
|
115
|
+
const written = [];
|
|
116
|
+
copyDir(tplDir, targetDir, tokens, written);
|
|
117
|
+
return { written, tokens };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ---- the CLI -----------------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
function ask(question) {
|
|
123
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
124
|
+
return new Promise((resolve) => rl.question(question, (answer) => { rl.close(); resolve(answer.trim()); }));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const fail = (message) => { console.error(`\n✖ ${message}\n`); process.exit(1); };
|
|
128
|
+
|
|
129
|
+
async function main(argv) {
|
|
130
|
+
const { name: argName, template } = parseArgs(argv);
|
|
131
|
+
|
|
132
|
+
let name = argName;
|
|
133
|
+
if (!name) name = await ask("Project name: ");
|
|
134
|
+
|
|
135
|
+
const { valid, problems } = validateName(name);
|
|
136
|
+
if (!valid) fail(problems.join("\n "));
|
|
137
|
+
|
|
138
|
+
const targetDir = path.resolve(process.cwd(), name);
|
|
139
|
+
try {
|
|
140
|
+
ensureTargetAvailable(targetDir);
|
|
141
|
+
} catch (err) {
|
|
142
|
+
fail(err.message);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
let result;
|
|
146
|
+
try {
|
|
147
|
+
result = scaffold({ name, template, targetDir });
|
|
148
|
+
} catch (err) {
|
|
149
|
+
fail(err.message);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
console.log(`\n✔ Created ${name} (${template} template) — ${result.written.length} files\n`);
|
|
153
|
+
console.log("Next steps:");
|
|
154
|
+
console.log(` cd ${name}`);
|
|
155
|
+
console.log(" npm install");
|
|
156
|
+
console.log(" npm run dev\n");
|
|
157
|
+
console.log("Zoijs works without this tool, too — it's only a convenience. See https://zoijs.dev/start\n");
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Run only when executed directly (resolve symlinks so the npm bin shim works).
|
|
161
|
+
function invokedDirectly() {
|
|
162
|
+
try {
|
|
163
|
+
return process.argv[1] && fs.realpathSync(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
164
|
+
} catch {
|
|
165
|
+
return process.argv[1] && pathToFileURL(process.argv[1]).href === import.meta.url;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (invokedDirectly()) main(process.argv.slice(2));
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-zoijs",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Create a new Zoijs app with one command. Plain HTML, CSS, and JavaScript — no build step.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"create-zoijs": "bin/create-zoijs.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"templates",
|
|
12
|
+
"README.md",
|
|
13
|
+
"CHANGELOG.md",
|
|
14
|
+
"LICENSE"
|
|
15
|
+
],
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=18"
|
|
18
|
+
},
|
|
19
|
+
"author": "Zoijs contributors (https://zoijs.dev)",
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"homepage": "https://zoijs.dev",
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "git+https://github.com/Zoijs/zoijs.git",
|
|
25
|
+
"directory": "create"
|
|
26
|
+
},
|
|
27
|
+
"bugs": {
|
|
28
|
+
"url": "https://github.com/Zoijs/zoijs/issues"
|
|
29
|
+
},
|
|
30
|
+
"keywords": [
|
|
31
|
+
"zoijs",
|
|
32
|
+
"create",
|
|
33
|
+
"create-zoijs",
|
|
34
|
+
"scaffold",
|
|
35
|
+
"starter",
|
|
36
|
+
"cli",
|
|
37
|
+
"no-build",
|
|
38
|
+
"no-jsx"
|
|
39
|
+
],
|
|
40
|
+
"scripts": {
|
|
41
|
+
"test": "node --test"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# {{APP_TITLE}}
|
|
2
|
+
|
|
3
|
+
A small task dashboard built with [Zoijs](https://zoijs.dev) — plain HTML, CSS,
|
|
4
|
+
and JavaScript. **No build step.**
|
|
5
|
+
|
|
6
|
+
## Develop
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npm install
|
|
10
|
+
npm run dev
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Then open the printed URL.
|
|
14
|
+
|
|
15
|
+
## Project layout
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
{{APP_NAME}}/
|
|
19
|
+
index.html import map + #app mount point
|
|
20
|
+
src/
|
|
21
|
+
app.js owns the task list; renders the page
|
|
22
|
+
style.css plain CSS
|
|
23
|
+
components/
|
|
24
|
+
Header.js parent → child: receives title + count
|
|
25
|
+
TaskItem.js child → parent: reports toggle/delete via callbacks
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## What it shows
|
|
29
|
+
|
|
30
|
+
- **`createState`** — the task list and the input draft.
|
|
31
|
+
- **`computed`** — the "tasks left" count, derived from the list.
|
|
32
|
+
- **`each`** — keyed rendering of the task list.
|
|
33
|
+
- **Parent → child** — `App` passes data down to `Header` as arguments.
|
|
34
|
+
- **Child → parent** — `TaskItem` reports events up through callbacks; `App`
|
|
35
|
+
owns the state and performs the changes.
|
|
36
|
+
|
|
37
|
+
No bundler, no build step. Open the files and read them. Learn more at
|
|
38
|
+
[zoijs.dev](https://zoijs.dev).
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
node_modules
|
|
@@ -0,0 +1,22 @@
|
|
|
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>{{APP_TITLE}}</title>
|
|
7
|
+
<link rel="stylesheet" href="./src/style.css" />
|
|
8
|
+
<!-- No build step: an import map points the @zoijs/core name at the package
|
|
9
|
+
installed in node_modules. `npm install` puts it there. -->
|
|
10
|
+
<script type="importmap">
|
|
11
|
+
{
|
|
12
|
+
"imports": {
|
|
13
|
+
"@zoijs/core": "./node_modules/@zoijs/core/src/index.js"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
</script>
|
|
17
|
+
</head>
|
|
18
|
+
<body>
|
|
19
|
+
<div id="app"></div>
|
|
20
|
+
<script type="module" src="./src/app.js"></script>
|
|
21
|
+
</body>
|
|
22
|
+
</html>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { html, mount, createState, computed, each } from "@zoijs/core";
|
|
2
|
+
import { Header } from "./components/Header.js";
|
|
3
|
+
import { TaskItem } from "./components/TaskItem.js";
|
|
4
|
+
|
|
5
|
+
// The App owns the task list. Children receive data as arguments (parent → child)
|
|
6
|
+
// and report events back through callbacks (child → parent). No store, no context.
|
|
7
|
+
function App() {
|
|
8
|
+
const tasks = createState([
|
|
9
|
+
{ id: 1, text: "Read the Zoijs docs", done: true },
|
|
10
|
+
{ id: 2, text: "Build something small", done: false },
|
|
11
|
+
{ id: 3, text: "Share it", done: false },
|
|
12
|
+
]);
|
|
13
|
+
let nextId = 4;
|
|
14
|
+
|
|
15
|
+
// computed: derived, cached, reactive.
|
|
16
|
+
const remaining = computed(() => tasks.get().filter((t) => !t.done).length);
|
|
17
|
+
|
|
18
|
+
const addTask = (text) => tasks.set([...tasks.get(), { id: nextId++, text, done: false }]);
|
|
19
|
+
const toggle = (id) => tasks.set(tasks.get().map((t) => (t.id === id ? { ...t, done: !t.done } : t)));
|
|
20
|
+
const remove = (id) => tasks.set(tasks.get().filter((t) => t.id !== id));
|
|
21
|
+
|
|
22
|
+
const draft = createState("");
|
|
23
|
+
const onSubmit = (e) => {
|
|
24
|
+
e.preventDefault();
|
|
25
|
+
const text = draft.get().trim();
|
|
26
|
+
if (!text) return;
|
|
27
|
+
addTask(text);
|
|
28
|
+
draft.set("");
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
return html`
|
|
32
|
+
<main>
|
|
33
|
+
${/* parent → child: pass plain data (title) and a reader (remaining) down */ ""}
|
|
34
|
+
${Header({ title: "{{APP_TITLE}}", remaining: () => remaining.get() })}
|
|
35
|
+
|
|
36
|
+
<form onsubmit=${onSubmit}>
|
|
37
|
+
<input
|
|
38
|
+
placeholder="Add a task…"
|
|
39
|
+
value=${() => draft.get()}
|
|
40
|
+
oninput=${(e) => draft.set(e.target.value)} />
|
|
41
|
+
<button type="submit">Add</button>
|
|
42
|
+
</form>
|
|
43
|
+
|
|
44
|
+
<ul class="tasks">
|
|
45
|
+
${each(
|
|
46
|
+
() => tasks.get(),
|
|
47
|
+
(t) => t.id,
|
|
48
|
+
(t) => TaskItem({ task: t, onToggle: toggle, onDelete: remove })
|
|
49
|
+
)}
|
|
50
|
+
</ul>
|
|
51
|
+
</main>
|
|
52
|
+
`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
mount(App, "#app");
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { html } from "@zoijs/core";
|
|
2
|
+
|
|
3
|
+
// Parent → child communication: Header receives plain data (`title`) and a
|
|
4
|
+
// reader function (`remaining`) as arguments. It owns no state of its own.
|
|
5
|
+
export function Header({ title, remaining }) {
|
|
6
|
+
return html`
|
|
7
|
+
<header>
|
|
8
|
+
<h1>${title}</h1>
|
|
9
|
+
<p class="count">
|
|
10
|
+
${() => remaining()} task${() => (remaining() === 1 ? "" : "s")} left
|
|
11
|
+
</p>
|
|
12
|
+
</header>
|
|
13
|
+
`;
|
|
14
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { html } from "@zoijs/core";
|
|
2
|
+
|
|
3
|
+
// Child → parent communication: TaskItem owns no state. It reports toggle and
|
|
4
|
+
// delete events upward by calling the callbacks the parent passed in.
|
|
5
|
+
export function TaskItem({ task, onToggle, onDelete }) {
|
|
6
|
+
return html`
|
|
7
|
+
<li class=${() => (task.done ? "done" : "")}>
|
|
8
|
+
<label>
|
|
9
|
+
<input
|
|
10
|
+
type="checkbox"
|
|
11
|
+
checked=${() => task.done}
|
|
12
|
+
onchange=${() => onToggle(task.id)} />
|
|
13
|
+
<span>${() => task.text}</span>
|
|
14
|
+
</label>
|
|
15
|
+
<button class="delete" title="Delete" onclick=${() => onDelete(task.id)}>✕</button>
|
|
16
|
+
</li>
|
|
17
|
+
`;
|
|
18
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
color-scheme: light dark;
|
|
3
|
+
font-family: system-ui, sans-serif;
|
|
4
|
+
line-height: 1.5;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
body {
|
|
8
|
+
margin: 0;
|
|
9
|
+
display: grid;
|
|
10
|
+
place-items: start center;
|
|
11
|
+
min-height: 100vh;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
main {
|
|
15
|
+
width: min(34rem, 92vw);
|
|
16
|
+
padding: 2rem 1rem;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
header h1 {
|
|
20
|
+
margin: 0 0 0.25rem;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.count {
|
|
24
|
+
margin: 0 0 1rem;
|
|
25
|
+
color: gray;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
form {
|
|
29
|
+
display: flex;
|
|
30
|
+
gap: 0.5rem;
|
|
31
|
+
margin-bottom: 1rem;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
input[type="text"],
|
|
35
|
+
form input {
|
|
36
|
+
flex: 1;
|
|
37
|
+
font: inherit;
|
|
38
|
+
padding: 0.5rem 0.7rem;
|
|
39
|
+
border: 1px solid rgba(127, 127, 127, 0.5);
|
|
40
|
+
border-radius: 8px;
|
|
41
|
+
background: transparent;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
button {
|
|
45
|
+
font: inherit;
|
|
46
|
+
padding: 0.5rem 0.9rem;
|
|
47
|
+
border: 1px solid currentColor;
|
|
48
|
+
border-radius: 8px;
|
|
49
|
+
background: transparent;
|
|
50
|
+
cursor: pointer;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.tasks {
|
|
54
|
+
list-style: none;
|
|
55
|
+
margin: 0;
|
|
56
|
+
padding: 0;
|
|
57
|
+
display: grid;
|
|
58
|
+
gap: 0.4rem;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.tasks li {
|
|
62
|
+
display: flex;
|
|
63
|
+
align-items: center;
|
|
64
|
+
justify-content: space-between;
|
|
65
|
+
gap: 0.5rem;
|
|
66
|
+
padding: 0.5rem 0.7rem;
|
|
67
|
+
border: 1px solid rgba(127, 127, 127, 0.25);
|
|
68
|
+
border-radius: 8px;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.tasks li.done span {
|
|
72
|
+
text-decoration: line-through;
|
|
73
|
+
opacity: 0.6;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.tasks label {
|
|
77
|
+
display: flex;
|
|
78
|
+
align-items: center;
|
|
79
|
+
gap: 0.5rem;
|
|
80
|
+
cursor: pointer;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.delete {
|
|
84
|
+
border: none;
|
|
85
|
+
padding: 0.2rem 0.5rem;
|
|
86
|
+
opacity: 0.6;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.delete:hover {
|
|
90
|
+
opacity: 1;
|
|
91
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# {{APP_TITLE}}
|
|
2
|
+
|
|
3
|
+
A [Zoijs](https://zoijs.dev) app — plain HTML, CSS, and JavaScript. **No build step.**
|
|
4
|
+
|
|
5
|
+
## Develop
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install
|
|
9
|
+
npm run dev
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Then open the printed URL.
|
|
13
|
+
|
|
14
|
+
## How it works
|
|
15
|
+
|
|
16
|
+
- **`index.html`** — an import map points `@zoijs/core` at `node_modules`, then loads `src/app.js`.
|
|
17
|
+
- **`src/app.js`** — your app: `createState` for state, `html` for markup, `mount` to render.
|
|
18
|
+
- **`src/style.css`** — plain CSS.
|
|
19
|
+
|
|
20
|
+
There's no bundler and no build step. Open the files and read them — that's the
|
|
21
|
+
whole app. Edit `src/app.js` and reload.
|
|
22
|
+
|
|
23
|
+
Learn more at [zoijs.dev](https://zoijs.dev).
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
node_modules
|
|
@@ -0,0 +1,22 @@
|
|
|
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>{{APP_TITLE}}</title>
|
|
7
|
+
<link rel="stylesheet" href="./src/style.css" />
|
|
8
|
+
<!-- No build step: an import map points the @zoijs/core name at the package
|
|
9
|
+
installed in node_modules. `npm install` puts it there. -->
|
|
10
|
+
<script type="importmap">
|
|
11
|
+
{
|
|
12
|
+
"imports": {
|
|
13
|
+
"@zoijs/core": "./node_modules/@zoijs/core/src/index.js"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
</script>
|
|
17
|
+
</head>
|
|
18
|
+
<body>
|
|
19
|
+
<div id="app"></div>
|
|
20
|
+
<script type="module" src="./src/app.js"></script>
|
|
21
|
+
</body>
|
|
22
|
+
</html>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { html, mount, createState } from "@zoijs/core";
|
|
2
|
+
|
|
3
|
+
// A complete Zoijs app: state, markup, and one mount call. No build step.
|
|
4
|
+
function App() {
|
|
5
|
+
const count = createState(0);
|
|
6
|
+
|
|
7
|
+
return html`
|
|
8
|
+
<main>
|
|
9
|
+
<h1>{{APP_TITLE}}</h1>
|
|
10
|
+
<p>Edit <code>src/app.js</code> and reload.</p>
|
|
11
|
+
<button onclick=${() => count.set(count.get() + 1)}>
|
|
12
|
+
Clicked ${() => count.get()} times
|
|
13
|
+
</button>
|
|
14
|
+
</main>
|
|
15
|
+
`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
mount(App, "#app");
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
color-scheme: light dark;
|
|
3
|
+
font-family: system-ui, sans-serif;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
body {
|
|
7
|
+
margin: 0;
|
|
8
|
+
display: grid;
|
|
9
|
+
place-items: center;
|
|
10
|
+
min-height: 100vh;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
main {
|
|
14
|
+
text-align: center;
|
|
15
|
+
padding: 2rem;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
h1 {
|
|
19
|
+
margin: 0 0 0.5rem;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
button {
|
|
23
|
+
font: inherit;
|
|
24
|
+
padding: 0.6rem 1.2rem;
|
|
25
|
+
border: 1px solid currentColor;
|
|
26
|
+
border-radius: 8px;
|
|
27
|
+
background: transparent;
|
|
28
|
+
cursor: pointer;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
button:hover {
|
|
32
|
+
background: rgba(127, 127, 127, 0.12);
|
|
33
|
+
}
|