@zvndev/darkmown 1.0.0 → 1.2.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 +25 -0
- package/README.md +64 -3
- package/docs/cli.md +46 -16
- package/docs/spec-alignment.md +16 -3
- package/package.json +2 -2
- package/site/_/roadmap.json +3 -1
- package/site/pages/app.wd +1 -1
- package/site/pages/data.wd +2 -2
- package/site/pages/docs/index.wd +22 -0
- package/site/pages/folio/cart.wd +2 -2
- package/site/pages/forge/index.wd +1 -1
- package/site/pages/interactions/index.skin +113 -0
- package/site/pages/interactions/index.wd +86 -0
- package/site/pages/ledger/index.wd +10 -1
- package/src/api-runner.js +219 -0
- package/src/behaviors/carousel.js +153 -0
- package/src/behaviors/sortable.js +175 -0
- package/src/builder.js +199 -4
- package/src/cli.js +79 -54
- package/src/compiler/body.js +78 -3
- package/src/compiler/context.js +12 -1
- package/src/compiler/directives.js +123 -0
- package/src/compiler/interpolation.js +15 -14
- package/src/compiler/loops.js +57 -8
- package/src/compiler/page.js +9 -3
- package/src/deploy.js +174 -0
- package/src/headers.js +4 -0
- package/src/runtime.js +3 -1
- package/src/scaffold.js +79 -147
- package/src/templates/blog/README.md +25 -0
- package/src/templates/blog/site/_/nav.wd +6 -0
- package/src/templates/blog/site/_/posts.json +16 -0
- package/src/templates/blog/site/pages/index.skin +51 -0
- package/src/templates/blog/site/pages/index.wd +24 -0
- package/src/templates/blog/site/pages/posts/hello-darkmown.md +16 -0
- package/src/templates/blog/site/pages/posts/markdown-native.md +20 -0
- package/src/templates/dashboard/README.md +25 -0
- package/src/templates/dashboard/api/metrics.js +15 -0
- package/src/templates/dashboard/site/_/nav.wd +6 -0
- package/src/templates/dashboard/site/pages/index.skin +74 -0
- package/src/templates/dashboard/site/pages/index.wd +75 -0
- package/src/templates/landing/README.md +25 -0
- package/src/templates/landing/site/_/nav.wd +6 -0
- package/src/templates/landing/site/pages/index.skin +140 -0
- package/src/templates/landing/site/pages/index.wd +54 -0
- package/src/templates/starter/README.md +32 -0
- package/src/templates/starter/site/_/nav.wd +7 -0
- package/src/templates/starter/site/pages/about.md +10 -0
- package/src/templates/starter/site/pages/index.skin +47 -0
- package/src/templates/starter/site/pages/index.wd +27 -0
- package/src/templates/store/README.md +28 -0
- package/src/templates/store/api/checkout.js +18 -0
- package/src/templates/store/site/_/nav.wd +6 -0
- package/src/templates/store/site/_/products.json +8 -0
- package/src/templates/store/site/pages/index.skin +100 -0
- package/src/templates/store/site/pages/index.wd +83 -0
package/src/scaffold.js
CHANGED
|
@@ -1,24 +1,60 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const templatesDir = path.join(moduleDir, "templates");
|
|
3
7
|
|
|
4
8
|
const darkmownVersion = JSON.parse(
|
|
5
9
|
fs.readFileSync(new URL("../package.json", import.meta.url), "utf8")
|
|
6
10
|
).version;
|
|
7
11
|
|
|
8
12
|
/**
|
|
9
|
-
*
|
|
13
|
+
* List the available scaffold templates (directory names under `src/templates`).
|
|
14
|
+
* @returns {string[]}
|
|
15
|
+
*/
|
|
16
|
+
export function availableTemplates() {
|
|
17
|
+
if (!fs.existsSync(templatesDir)) return [];
|
|
18
|
+
return fs
|
|
19
|
+
.readdirSync(templatesDir, { withFileTypes: true })
|
|
20
|
+
.filter((entry) => entry.isDirectory())
|
|
21
|
+
.map((entry) => entry.name)
|
|
22
|
+
.sort();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Scaffold a new Darkmown project into `root` from a template, creating files
|
|
27
|
+
* that don't already exist (never overwriting). `package.json` is generated (so
|
|
28
|
+
* the name tracks the directory and the `@zvndev/darkmown` dep pins the installed
|
|
29
|
+
* version); every other file is copied verbatim from the chosen template.
|
|
10
30
|
* @param {string} root Absolute path to the target project directory.
|
|
11
|
-
* @
|
|
31
|
+
* @param {{ template?: string }} [options] `template` defaults to `"starter"`.
|
|
32
|
+
* @returns {{ root: string, template: string }}
|
|
12
33
|
*/
|
|
13
|
-
export function initProject(root) {
|
|
34
|
+
export function initProject(root, options = {}) {
|
|
35
|
+
const template = options.template || "starter";
|
|
36
|
+
const templateDir = path.join(templatesDir, template);
|
|
37
|
+
if (!fs.existsSync(templateDir) || !fs.statSync(templateDir).isDirectory()) {
|
|
38
|
+
const list = availableTemplates().join(", ");
|
|
39
|
+
throw new Error(
|
|
40
|
+
`Unknown template "${template}". Available templates: ${list || "(none)"}. ` +
|
|
41
|
+
"Use: darkmown init [dir] --template <name>"
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
14
45
|
fs.mkdirSync(root, { recursive: true });
|
|
46
|
+
|
|
47
|
+
// package.json is generated, not copied: the name follows the target directory,
|
|
48
|
+
// the dep pins the installed Darkmown version, and `type: "module"` makes a
|
|
49
|
+
// project `api/*.js` import as ESM in `darkmown dev` and on every host.
|
|
15
50
|
writeNew(
|
|
16
51
|
root,
|
|
17
52
|
"package.json",
|
|
18
|
-
JSON.stringify(
|
|
53
|
+
`${JSON.stringify(
|
|
19
54
|
{
|
|
20
55
|
name: packageNameFromRoot(root),
|
|
21
56
|
private: true,
|
|
57
|
+
type: "module",
|
|
22
58
|
scripts: {
|
|
23
59
|
dev: "darkmown dev",
|
|
24
60
|
build: "darkmown build",
|
|
@@ -30,161 +66,57 @@ export function initProject(root) {
|
|
|
30
66
|
},
|
|
31
67
|
null,
|
|
32
68
|
2
|
|
33
|
-
)
|
|
34
|
-
);
|
|
35
|
-
writeNew(
|
|
36
|
-
root,
|
|
37
|
-
"site/pages/index.wd",
|
|
38
|
-
[
|
|
39
|
-
"---",
|
|
40
|
-
"title: My Darkmown site",
|
|
41
|
-
"---",
|
|
42
|
-
"",
|
|
43
|
-
"@include /nav.wd",
|
|
44
|
-
"",
|
|
45
|
-
"<main>",
|
|
46
|
-
"",
|
|
47
|
-
"# My Darkmown site",
|
|
48
|
-
"",
|
|
49
|
-
"Plain Markdown works. Rename to `.wd` when you want directives.",
|
|
50
|
-
"",
|
|
51
|
-
":state count = 0",
|
|
52
|
-
"",
|
|
53
|
-
"Count: { count }",
|
|
54
|
-
"",
|
|
55
|
-
':button "Increment" -> count++',
|
|
56
|
-
"",
|
|
57
|
-
':state todos = ["Write a page"]',
|
|
58
|
-
"",
|
|
59
|
-
"@loop todos into todo",
|
|
60
|
-
"- { todo }",
|
|
61
|
-
"@endloop",
|
|
62
|
-
"",
|
|
63
|
-
':button "Add" -> todos += "Add a loop"',
|
|
64
|
-
"",
|
|
65
|
-
"</main>"
|
|
66
|
-
].join("\n")
|
|
67
|
-
);
|
|
68
|
-
writeNew(
|
|
69
|
-
root,
|
|
70
|
-
"site/pages/index.skin",
|
|
71
|
-
[
|
|
72
|
-
"tokens",
|
|
73
|
-
" ink #172026",
|
|
74
|
-
" paper #f7f2e8",
|
|
75
|
-
" accent #0f8b8d",
|
|
76
|
-
"",
|
|
77
|
-
"page",
|
|
78
|
-
" margin 0",
|
|
79
|
-
" font ui-sans-serif, system-ui, sans-serif",
|
|
80
|
-
" color $ink",
|
|
81
|
-
" bg $paper",
|
|
82
|
-
"",
|
|
83
|
-
"main",
|
|
84
|
-
" max-width 760px",
|
|
85
|
-
" margin 0 auto",
|
|
86
|
-
" padding 3rem 1.5rem",
|
|
87
|
-
"",
|
|
88
|
-
"nav",
|
|
89
|
-
" display flex",
|
|
90
|
-
" align-items center",
|
|
91
|
-
" justify-content space-between",
|
|
92
|
-
" gap 1rem",
|
|
93
|
-
" max-width 760px",
|
|
94
|
-
" margin 0 auto",
|
|
95
|
-
" padding 1.25rem 1.5rem",
|
|
96
|
-
"",
|
|
97
|
-
"nav strong",
|
|
98
|
-
" font-size 1.1rem",
|
|
99
|
-
"",
|
|
100
|
-
"nav .links",
|
|
101
|
-
" display flex",
|
|
102
|
-
" gap 1.25rem",
|
|
103
|
-
"",
|
|
104
|
-
"nav a",
|
|
105
|
-
" color $ink",
|
|
106
|
-
" text-decoration none",
|
|
107
|
-
" opacity .8",
|
|
108
|
-
"",
|
|
109
|
-
"nav a:hover",
|
|
110
|
-
" opacity 1",
|
|
111
|
-
" color $accent",
|
|
112
|
-
"",
|
|
113
|
-
"button",
|
|
114
|
-
" bg $accent",
|
|
115
|
-
" color white",
|
|
116
|
-
" border 0",
|
|
117
|
-
" padding .75rem 1rem",
|
|
118
|
-
" radius 8px"
|
|
119
|
-
].join("\n")
|
|
120
|
-
);
|
|
121
|
-
writeNew(
|
|
122
|
-
root,
|
|
123
|
-
"site/_/nav.wd",
|
|
124
|
-
[
|
|
125
|
-
"<nav>",
|
|
126
|
-
" <strong>My site</strong>",
|
|
127
|
-
' <span class="links">',
|
|
128
|
-
' <a href="/">Home</a>',
|
|
129
|
-
' <a href="/about/">About</a>',
|
|
130
|
-
" </span>",
|
|
131
|
-
"</nav>"
|
|
132
|
-
].join("\n")
|
|
69
|
+
)}\n`
|
|
133
70
|
);
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
"title: About",
|
|
140
|
-
"---",
|
|
141
|
-
"",
|
|
142
|
-
"# About",
|
|
143
|
-
"",
|
|
144
|
-
"This page is plain Markdown (`.md`) — strict CommonMark, zero framework JavaScript.",
|
|
145
|
-
"Rename it to `.wd` when you want directives like state, loops, or forms.",
|
|
146
|
-
"",
|
|
147
|
-
"[Back home](/)"
|
|
148
|
-
].join("\n")
|
|
149
|
-
);
|
|
150
|
-
writeNew(
|
|
151
|
-
root,
|
|
152
|
-
"README.md",
|
|
153
|
-
[
|
|
154
|
-
"# My Darkmown site",
|
|
155
|
-
"",
|
|
156
|
-
"Run `npm install` and `npm run dev` to start the live compiler.",
|
|
157
|
-
"",
|
|
158
|
-
"## Commands",
|
|
159
|
-
"",
|
|
160
|
-
"- `npm run dev` — live compiler on http://localhost:5173 with hot reload.",
|
|
161
|
-
"- `npm run build` — compile the site to `dist/`.",
|
|
162
|
-
"- `npm run preview` — serve the built `dist/` locally.",
|
|
163
|
-
"",
|
|
164
|
-
"## Layout",
|
|
165
|
-
"",
|
|
166
|
-
"- Pages live in `site/pages`.",
|
|
167
|
-
"- Shared includes live in `site/_`.",
|
|
168
|
-
"- Hidden route files start with `.` or `-`.",
|
|
169
|
-
"- Colocated `index.skin` and `index.js` attach automatically.",
|
|
170
|
-
"- Any other file under `site/pages` (images, fonts, …) copies to `dist/` at its own path."
|
|
171
|
-
].join("\n")
|
|
172
|
-
);
|
|
173
|
-
return { root };
|
|
71
|
+
|
|
72
|
+
for (const rel of walk(templateDir)) {
|
|
73
|
+
copyNew(path.join(templateDir, rel), path.join(root, rel));
|
|
74
|
+
}
|
|
75
|
+
return { root, template };
|
|
174
76
|
}
|
|
175
77
|
|
|
176
78
|
/**
|
|
177
|
-
* Write a file relative to `root` only if it does not already exist.
|
|
79
|
+
* Write a file relative to `root` only if it does not already exist. Content is
|
|
80
|
+
* written verbatim (callers include any trailing newline).
|
|
178
81
|
* @param {string} root Project root directory.
|
|
179
82
|
* @param {string} file Path relative to `root`.
|
|
180
|
-
* @param {string} content File contents
|
|
83
|
+
* @param {string} content File contents.
|
|
181
84
|
* @returns {void}
|
|
182
85
|
*/
|
|
183
86
|
function writeNew(root, file, content) {
|
|
184
87
|
const target = path.join(root, file);
|
|
185
88
|
if (fs.existsSync(target)) return;
|
|
186
89
|
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
187
|
-
fs.writeFileSync(target,
|
|
90
|
+
fs.writeFileSync(target, content);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Copy a template file to `dest`, preserving bytes, only if `dest` is absent.
|
|
95
|
+
* @param {string} source
|
|
96
|
+
* @param {string} dest
|
|
97
|
+
* @returns {void}
|
|
98
|
+
*/
|
|
99
|
+
function copyNew(source, dest) {
|
|
100
|
+
if (fs.existsSync(dest)) return;
|
|
101
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
102
|
+
fs.copyFileSync(source, dest);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Relative paths of every file under `dir` (POSIX-separated), recursively.
|
|
107
|
+
* @param {string} dir
|
|
108
|
+
* @param {string} [base]
|
|
109
|
+
* @returns {string[]}
|
|
110
|
+
*/
|
|
111
|
+
function walk(dir, base = dir) {
|
|
112
|
+
/** @type {string[]} */
|
|
113
|
+
const files = [];
|
|
114
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
115
|
+
const abs = path.join(dir, entry.name);
|
|
116
|
+
if (entry.isDirectory()) files.push(...walk(abs, base));
|
|
117
|
+
else files.push(path.relative(base, abs).replaceAll(path.sep, "/"));
|
|
118
|
+
}
|
|
119
|
+
return files;
|
|
188
120
|
}
|
|
189
121
|
|
|
190
122
|
/**
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Blog — a Darkmown template
|
|
2
|
+
|
|
3
|
+
A markdown-native blog. Posts are plain `.md` files in `site/pages/posts/`; the
|
|
4
|
+
home page loops `site/_/posts.json` to list them. Static pages, zero JavaScript.
|
|
5
|
+
|
|
6
|
+
## Add a post
|
|
7
|
+
|
|
8
|
+
1. Create `site/pages/posts/<slug>.md` with a `title` in frontmatter.
|
|
9
|
+
2. Add an entry to `site/_/posts.json` (`slug`, `url`, `title`, `date`, `excerpt`).
|
|
10
|
+
|
|
11
|
+
## Commands
|
|
12
|
+
|
|
13
|
+
- `npm run dev` — live compiler on http://localhost:5173.
|
|
14
|
+
- `npm run build` — compile to `dist/`.
|
|
15
|
+
- `npm run preview` — serve the built `dist/` locally.
|
|
16
|
+
|
|
17
|
+
## Deploy
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
npx darkmown deploy vercel # or: npx darkmown deploy cloudflare
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
[](https://vercel.com/new/clone?repository-url=YOUR_REPO_URL)
|
|
24
|
+
|
|
25
|
+
[](https://deploy.workers.cloudflare.com/?url=YOUR_REPO_URL)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"slug": "hello-darkmown",
|
|
4
|
+
"url": "/posts/hello-darkmown/",
|
|
5
|
+
"title": "Hello, Darkmown",
|
|
6
|
+
"date": "2026-06-15",
|
|
7
|
+
"excerpt": "Why I rewrote my blog as plain Markdown files — and shipped zero framework JavaScript."
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"slug": "markdown-native",
|
|
11
|
+
"url": "/posts/markdown-native/",
|
|
12
|
+
"title": "Markdown-native, by default",
|
|
13
|
+
"date": "2026-06-01",
|
|
14
|
+
"excerpt": "Every post is a .md file. The index is one .wd loop over this manifest. That's the whole site."
|
|
15
|
+
}
|
|
16
|
+
]
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
tokens
|
|
2
|
+
ink #1d2530
|
|
3
|
+
paper #fbfaf7
|
|
4
|
+
muted #6b7785
|
|
5
|
+
accent #2f6df0
|
|
6
|
+
|
|
7
|
+
page
|
|
8
|
+
margin 0
|
|
9
|
+
font ui-sans-serif, system-ui, sans-serif
|
|
10
|
+
color $ink
|
|
11
|
+
bg $paper
|
|
12
|
+
line-height 1.6
|
|
13
|
+
|
|
14
|
+
main
|
|
15
|
+
max-width 720px
|
|
16
|
+
margin 0 auto
|
|
17
|
+
padding 2.5rem 1.5rem
|
|
18
|
+
|
|
19
|
+
nav
|
|
20
|
+
display flex
|
|
21
|
+
align-items center
|
|
22
|
+
justify-content space-between
|
|
23
|
+
max-width 720px
|
|
24
|
+
margin 0 auto
|
|
25
|
+
padding 1.25rem 1.5rem
|
|
26
|
+
|
|
27
|
+
nav strong
|
|
28
|
+
font-size 1.1rem
|
|
29
|
+
|
|
30
|
+
nav a
|
|
31
|
+
color $accent
|
|
32
|
+
text-decoration none
|
|
33
|
+
|
|
34
|
+
.post-card
|
|
35
|
+
padding 1.5rem 0
|
|
36
|
+
border-bottom 1px solid #e7e3da
|
|
37
|
+
|
|
38
|
+
.post-card h3
|
|
39
|
+
margin 0 0 .25rem
|
|
40
|
+
|
|
41
|
+
.post-card a
|
|
42
|
+
color $ink
|
|
43
|
+
text-decoration none
|
|
44
|
+
|
|
45
|
+
.post-card a:hover
|
|
46
|
+
color $accent
|
|
47
|
+
|
|
48
|
+
.post-meta
|
|
49
|
+
margin 0 0 .5rem
|
|
50
|
+
color $muted
|
|
51
|
+
font-size .9rem
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: My Blog
|
|
3
|
+
description: A markdown-native blog built with Darkmown.
|
|
4
|
+
transitions: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
@include /nav.wd
|
|
8
|
+
|
|
9
|
+
<main>
|
|
10
|
+
|
|
11
|
+
# My Blog
|
|
12
|
+
|
|
13
|
+
Posts are plain Markdown files in `site/pages/posts/`. This index is one `.wd`
|
|
14
|
+
loop over a small JSON manifest — sorted newest-first, shipping zero JavaScript.
|
|
15
|
+
|
|
16
|
+
@loop /posts.json into post sort by post.date desc
|
|
17
|
+
::: post-card
|
|
18
|
+
### [{ post.title }]({ post.url })
|
|
19
|
+
<p class="post-meta">{ post.date }</p>
|
|
20
|
+
{ post.excerpt }
|
|
21
|
+
:::
|
|
22
|
+
@endloop
|
|
23
|
+
|
|
24
|
+
</main>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Hello, Darkmown
|
|
3
|
+
description: Why I rewrote my blog as plain Markdown files.
|
|
4
|
+
transitions: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Hello, Darkmown
|
|
8
|
+
|
|
9
|
+
This post is a plain `.md` file — strict CommonMark, **zero** framework
|
|
10
|
+
JavaScript. It loads instantly and is trivial to edit.
|
|
11
|
+
|
|
12
|
+
When I want something interactive — a like button, a live filter, a form — I
|
|
13
|
+
rename a file to `.wd` and add a directive. Static stays static; only the pages
|
|
14
|
+
that need it ship the tiny reactive runtime.
|
|
15
|
+
|
|
16
|
+
[← Back to all posts](/)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Markdown-native, by default
|
|
3
|
+
description: Every post is a .md file; the index is one .wd loop.
|
|
4
|
+
transitions: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Markdown-native, by default
|
|
8
|
+
|
|
9
|
+
To add a post, drop a Markdown file in `site/pages/posts/` and add a line to
|
|
10
|
+
`site/_/posts.json`. The home page loops that manifest:
|
|
11
|
+
|
|
12
|
+
```wd
|
|
13
|
+
@loop /posts.json into post sort by post.date desc
|
|
14
|
+
### [{ post.title }]({ post.url })
|
|
15
|
+
@endloop
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
No build config, no plugins, no content layer. The file *is* the page.
|
|
19
|
+
|
|
20
|
+
[← Back to all posts](/)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Dashboard — a Darkmown template
|
|
2
|
+
|
|
3
|
+
A live dashboard. The page is static; the stat cards `:fetch` their data from
|
|
4
|
+
`api/metrics.js` — a plain Web-standard `(request) => Response` function that runs
|
|
5
|
+
in `darkmown dev` and on every host. Loading and error states are handled inline.
|
|
6
|
+
|
|
7
|
+
- **Data** — `api/metrics.js`. Replace its body with a real query (DB, analytics,
|
|
8
|
+
billing). Add more endpoints as `api/<name>.js`; each maps to `/api/<name>`.
|
|
9
|
+
- **View** — `site/pages/index.wd` fetches `/api/metrics` and renders stat cards.
|
|
10
|
+
|
|
11
|
+
## Commands
|
|
12
|
+
|
|
13
|
+
- `npm run dev` — live compiler + local `/api/*` runner on http://localhost:5173.
|
|
14
|
+
- `npm run build` — compile to `dist/`.
|
|
15
|
+
- `npm run preview` — serve the built `dist/` locally (static only; use `dev` for `/api`).
|
|
16
|
+
|
|
17
|
+
## Deploy
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
npx darkmown deploy vercel # or: npx darkmown deploy cloudflare
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
[](https://vercel.com/new/clone?repository-url=YOUR_REPO_URL)
|
|
24
|
+
|
|
25
|
+
[](https://deploy.workers.cloudflare.com/?url=YOUR_REPO_URL)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Demo metrics endpoint. A plain Web-standard `(request) => Response` handler:
|
|
2
|
+
// it runs in `darkmown dev` and on Vercel Edge / Cloudflare Pages with no extra
|
|
3
|
+
// config. Returns a snapshot of stats — replace the body with a real query
|
|
4
|
+
// (database, analytics API, billing provider, …).
|
|
5
|
+
export const config = { runtime: "edge" };
|
|
6
|
+
|
|
7
|
+
export default function () {
|
|
8
|
+
return Response.json({
|
|
9
|
+
revenue: 48230,
|
|
10
|
+
orders: 312,
|
|
11
|
+
visitors: 8940,
|
|
12
|
+
conversion: 3.5,
|
|
13
|
+
updatedAt: new Date().toISOString()
|
|
14
|
+
});
|
|
15
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
tokens
|
|
2
|
+
ink #0f1722
|
|
3
|
+
paper #0b1018
|
|
4
|
+
panel #131c28
|
|
5
|
+
line #243245
|
|
6
|
+
muted #8aa0b8
|
|
7
|
+
accent #4f9cff
|
|
8
|
+
|
|
9
|
+
page
|
|
10
|
+
margin 0
|
|
11
|
+
font ui-sans-serif, system-ui, sans-serif
|
|
12
|
+
color #e7eefc
|
|
13
|
+
bg $paper
|
|
14
|
+
line-height 1.55
|
|
15
|
+
|
|
16
|
+
main
|
|
17
|
+
max-width 880px
|
|
18
|
+
margin 0 auto
|
|
19
|
+
padding 2rem 1.5rem
|
|
20
|
+
|
|
21
|
+
nav
|
|
22
|
+
display flex
|
|
23
|
+
align-items center
|
|
24
|
+
justify-content space-between
|
|
25
|
+
max-width 880px
|
|
26
|
+
margin 0 auto
|
|
27
|
+
padding 1.25rem 1.5rem
|
|
28
|
+
|
|
29
|
+
nav strong
|
|
30
|
+
font-size 1.1rem
|
|
31
|
+
|
|
32
|
+
nav a
|
|
33
|
+
color $accent
|
|
34
|
+
text-decoration none
|
|
35
|
+
|
|
36
|
+
.stat-grid
|
|
37
|
+
display grid
|
|
38
|
+
grid-template-columns repeat(auto-fit, minmax(160px, 1fr))
|
|
39
|
+
gap 1rem
|
|
40
|
+
margin 1.5rem 0
|
|
41
|
+
|
|
42
|
+
.stat
|
|
43
|
+
background $panel
|
|
44
|
+
border 1px solid $line
|
|
45
|
+
radius 12px
|
|
46
|
+
padding 1.1rem 1.25rem
|
|
47
|
+
|
|
48
|
+
.stat-label
|
|
49
|
+
color $muted
|
|
50
|
+
font-size .85rem
|
|
51
|
+
text-transform uppercase
|
|
52
|
+
letter-spacing .04em
|
|
53
|
+
|
|
54
|
+
.stat-num
|
|
55
|
+
font-size 1.9rem
|
|
56
|
+
font-weight 700
|
|
57
|
+
margin-top .35rem
|
|
58
|
+
|
|
59
|
+
.note
|
|
60
|
+
background $panel
|
|
61
|
+
border 1px solid $line
|
|
62
|
+
radius 10px
|
|
63
|
+
padding 1rem
|
|
64
|
+
|
|
65
|
+
.error
|
|
66
|
+
background #2a1620
|
|
67
|
+
border 1px solid #5b2738
|
|
68
|
+
radius 10px
|
|
69
|
+
padding 1rem
|
|
70
|
+
|
|
71
|
+
.updated
|
|
72
|
+
color $muted
|
|
73
|
+
font-size .85rem
|
|
74
|
+
margin-top .5rem
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Dashboard — built with Darkmown
|
|
3
|
+
description: A live dashboard — stat cards fetched from a serverless function, on an otherwise static page.
|
|
4
|
+
transitions: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
@include /nav.wd
|
|
8
|
+
|
|
9
|
+
<main>
|
|
10
|
+
|
|
11
|
+
::: section #dash
|
|
12
|
+
|
|
13
|
+
# Dashboard
|
|
14
|
+
|
|
15
|
+
:fetch metrics from "/api/metrics"
|
|
16
|
+
|
|
17
|
+
:if metrics_loading
|
|
18
|
+
::: card .note
|
|
19
|
+
Loading metrics…
|
|
20
|
+
:::
|
|
21
|
+
:endif
|
|
22
|
+
|
|
23
|
+
:if metrics_error
|
|
24
|
+
::: card .error
|
|
25
|
+
Couldn't load metrics: { metrics_error }
|
|
26
|
+
:::
|
|
27
|
+
:endif
|
|
28
|
+
|
|
29
|
+
:if metrics
|
|
30
|
+
::: stat-grid
|
|
31
|
+
::: card .stat
|
|
32
|
+
::: stat-label
|
|
33
|
+
Revenue
|
|
34
|
+
:::
|
|
35
|
+
::: stat-num
|
|
36
|
+
{ metrics.revenue | money }
|
|
37
|
+
:::
|
|
38
|
+
:::
|
|
39
|
+
::: card .stat
|
|
40
|
+
::: stat-label
|
|
41
|
+
Orders
|
|
42
|
+
:::
|
|
43
|
+
::: stat-num
|
|
44
|
+
{ metrics.orders }
|
|
45
|
+
:::
|
|
46
|
+
:::
|
|
47
|
+
::: card .stat
|
|
48
|
+
::: stat-label
|
|
49
|
+
Visitors
|
|
50
|
+
:::
|
|
51
|
+
::: stat-num
|
|
52
|
+
{ metrics.visitors }
|
|
53
|
+
:::
|
|
54
|
+
:::
|
|
55
|
+
::: card .stat
|
|
56
|
+
::: stat-label
|
|
57
|
+
Conversion
|
|
58
|
+
:::
|
|
59
|
+
::: stat-num
|
|
60
|
+
{ metrics.conversion }%
|
|
61
|
+
:::
|
|
62
|
+
:::
|
|
63
|
+
:::
|
|
64
|
+
|
|
65
|
+
::: updated
|
|
66
|
+
Updated { metrics.updatedAt }
|
|
67
|
+
:::
|
|
68
|
+
:endif
|
|
69
|
+
|
|
70
|
+
The numbers come from `api/metrics.js` — a serverless function that runs in
|
|
71
|
+
`darkmown dev` and on your host. The page itself stays static; only the data fetches.
|
|
72
|
+
|
|
73
|
+
:::
|
|
74
|
+
|
|
75
|
+
</main>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Landing — a Darkmown template
|
|
2
|
+
|
|
3
|
+
A marketing one-pager: a hero, a feature grid, and a testimonials **carousel**.
|
|
4
|
+
The static parts ship zero framework JavaScript; only the carousel loads a tiny,
|
|
5
|
+
pay-for-what-you-use behavior module (`/__wd/behaviors/carousel.js`).
|
|
6
|
+
|
|
7
|
+
- **Carousel** — the `:carousel … :endcarousel` block; native CSS scroll-snap +
|
|
8
|
+
the behavior add prev/next, dots, and mouse drag. Touch swipe is native.
|
|
9
|
+
- **Everything else** — plain Markdown + `:::` containers, no JavaScript.
|
|
10
|
+
|
|
11
|
+
## Commands
|
|
12
|
+
|
|
13
|
+
- `npm run dev` — live compiler on http://localhost:5173.
|
|
14
|
+
- `npm run build` — compile to `dist/`.
|
|
15
|
+
- `npm run preview` — serve the built `dist/` locally.
|
|
16
|
+
|
|
17
|
+
## Deploy
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
npx darkmown deploy vercel # or: npx darkmown deploy cloudflare
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
[](https://vercel.com/new/clone?repository-url=YOUR_REPO_URL)
|
|
24
|
+
|
|
25
|
+
[](https://deploy.workers.cloudflare.com/?url=YOUR_REPO_URL)
|