@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.
Files changed (56) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +64 -3
  3. package/docs/cli.md +46 -16
  4. package/docs/spec-alignment.md +16 -3
  5. package/package.json +2 -2
  6. package/site/_/roadmap.json +3 -1
  7. package/site/pages/app.wd +1 -1
  8. package/site/pages/data.wd +2 -2
  9. package/site/pages/docs/index.wd +22 -0
  10. package/site/pages/folio/cart.wd +2 -2
  11. package/site/pages/forge/index.wd +1 -1
  12. package/site/pages/interactions/index.skin +113 -0
  13. package/site/pages/interactions/index.wd +86 -0
  14. package/site/pages/ledger/index.wd +10 -1
  15. package/src/api-runner.js +219 -0
  16. package/src/behaviors/carousel.js +153 -0
  17. package/src/behaviors/sortable.js +175 -0
  18. package/src/builder.js +199 -4
  19. package/src/cli.js +79 -54
  20. package/src/compiler/body.js +78 -3
  21. package/src/compiler/context.js +12 -1
  22. package/src/compiler/directives.js +123 -0
  23. package/src/compiler/interpolation.js +15 -14
  24. package/src/compiler/loops.js +57 -8
  25. package/src/compiler/page.js +9 -3
  26. package/src/deploy.js +174 -0
  27. package/src/headers.js +4 -0
  28. package/src/runtime.js +3 -1
  29. package/src/scaffold.js +79 -147
  30. package/src/templates/blog/README.md +25 -0
  31. package/src/templates/blog/site/_/nav.wd +6 -0
  32. package/src/templates/blog/site/_/posts.json +16 -0
  33. package/src/templates/blog/site/pages/index.skin +51 -0
  34. package/src/templates/blog/site/pages/index.wd +24 -0
  35. package/src/templates/blog/site/pages/posts/hello-darkmown.md +16 -0
  36. package/src/templates/blog/site/pages/posts/markdown-native.md +20 -0
  37. package/src/templates/dashboard/README.md +25 -0
  38. package/src/templates/dashboard/api/metrics.js +15 -0
  39. package/src/templates/dashboard/site/_/nav.wd +6 -0
  40. package/src/templates/dashboard/site/pages/index.skin +74 -0
  41. package/src/templates/dashboard/site/pages/index.wd +75 -0
  42. package/src/templates/landing/README.md +25 -0
  43. package/src/templates/landing/site/_/nav.wd +6 -0
  44. package/src/templates/landing/site/pages/index.skin +140 -0
  45. package/src/templates/landing/site/pages/index.wd +54 -0
  46. package/src/templates/starter/README.md +32 -0
  47. package/src/templates/starter/site/_/nav.wd +7 -0
  48. package/src/templates/starter/site/pages/about.md +10 -0
  49. package/src/templates/starter/site/pages/index.skin +47 -0
  50. package/src/templates/starter/site/pages/index.wd +27 -0
  51. package/src/templates/store/README.md +28 -0
  52. package/src/templates/store/api/checkout.js +18 -0
  53. package/src/templates/store/site/_/nav.wd +6 -0
  54. package/src/templates/store/site/_/products.json +8 -0
  55. package/src/templates/store/site/pages/index.skin +100 -0
  56. package/src/templates/store/site/pages/index.wd +83 -0
package/CHANGELOG.md CHANGED
@@ -2,6 +2,31 @@
2
2
 
3
3
  All notable changes to Darkmown are documented here. Versions follow [semver](https://semver.org); pre-1.0 minor versions may contain breaking changes.
4
4
 
5
+ ## 1.2.0 — 2026-06-27
6
+
7
+ Backends, deploy, and rich interactions — all additive. The core runtime is unchanged (still well under the 8 KB budget); the new behavior modules are budgeted separately so static pages stay zero-JS.
8
+
9
+ - **Serverless `api/` model.** Backend endpoints are plain-JS Web-standard handlers — `export default (request, context) => Response` — in a top-level `api/` directory (`api/echo.js` → `/api/echo`; `api/users/[id].js` → `/api/users/:id`). Darkmown owns no server and adds no backend DSL: it adapts to the host. The build stays 100% static. `darkmown dev` runs a local runner so `/api/*` behaves identically before you deploy; on **Vercel** the functions run natively (Edge); `darkmown build --target cloudflare` emits a `dist/_worker.js` that routes `/api/*` to the same functions and serves the rest from `env.ASSETS`. `form-action 'self'` was added to both CSP variants (and `vercel.json`) so a native `:form action="/api/…"` POST is allowed.
10
+ - **Templates + deploy CLI.** `darkmown init --template <starter|blog|store|dashboard|landing>` scaffolds from in-package templates — each a real, deployable site (`store`/`dashboard` ship an example `api/` function). `darkmown deploy <vercel|cloudflare> [--prod]` writes platform config, runs a target-aware build, wraps the platform CLI (`npx vercel` / `npx wrangler pages deploy`), and prints the deploy URL; an unauthenticated CLI surfaces the login to run.
11
+ - **Interaction behaviors (pay-for-what-you-use).** `:slider name = v min max step` is compile-time only — a range input bound through `:bind` (range values coerce to Number), shipping **no** JS. `:sortable` (a `@loop` clause) and `:carousel` compile to tiny `/__wd/behaviors/<name>.js` modules injected **only** on the pages that use them, budgeted **separately** from the core runtime in `.size-snapshot.json`. `:sortable` drag-reorders a plain reactive `:state`/`:store` loop via Pointer Events + Arrow keys through the public `window.wd` API; `:carousel` uses native CSS scroll-snap plus prev/next, dots, autoplay, and mouse drag. New `/interactions/` demo page.
12
+ - **Review hardening (a11y + backend).** Drag/keyboard feedback and focus now target the author's styled row (not the loop wrapper), so the dragging style and focus ring actually show; keyboard reorder gains screen-reader instructions, `aria-posinset`/`aria-setsize`, and a live "Moved to position N of M" announcement. The carousel drops an incomplete `tablist`/`tab` role for plain buttons with `aria-current`, suppresses autoplay under `prefers-reduced-motion` (and pauses when the tab is hidden). The dev API runner preserves multiple `Set-Cookie` headers (no comma-fold), matching production; `darkmown deploy` recognizes wrangler's real auth-failure wording and strips trailing punctuation from parsed URLs; the build warns on `api/` route collisions and on bare npm imports in Cloudflare advanced-mode handlers (which ship without a bundler).
13
+
14
+ ## 1.1.0 — 2026-06-27
15
+
16
+ Follow-up to 1.0: turn the two rough edges flagged at release into real fixes. No runtime change (still ~7.4 KB gzipped).
17
+
18
+ - **Multi-line `:state` / `:store` / `:theme` values.** An array or object seed may now span several lines for readability — open the `[`/`{` on the directive line and let it run until it balances:
19
+
20
+ ```wd
21
+ :store rows = [
22
+ {"id": 1, "label": "One"},
23
+ {"id": 2, "label": "Two"}
24
+ ]
25
+ ```
26
+
27
+ 1.0 made the old silent-corruption case (the literal stored verbatim as the string `"["`) a compile error; 1.1 makes the readable multi-line form actually *work*. The compiler joins the balanced literal (string- and escape-aware bracket counting) into one line before parsing. The literal must balance with no blank line inside it; an unterminated literal stays a clear compile error. Quote genuinely literal bracket text (`:state tag = "[draft]"`). The Ledger demo's seed data is now written this way.
28
+ - **Single source of truth for the runtime-size budget (internal).** The gzip budget had drifted into four hardcoded copies (`package.json`, `scripts/size-check.mjs`, `.github/workflows/ci.yml`, `tests/size.test.js`) — a mismatch in `ci.yml` failed the 1.0 PR. The budget now lives only in `.size-snapshot.json` (`runtime.budget`); `scripts/size-check.mjs` reads it and everything else delegates to that script. A `tests/size.test.js` drift guard fails if any config reintroduces a hardcoded budget or an inline size gate.
29
+
5
30
  ## 1.0.0 — 2026-06-27
6
31
 
7
32
  The 1.0 release. The directive surface is now stable, and this version closes the biggest "I'd have to drop to raw HTML/JS for that" gaps an audit found — display formatting, time, an explicit theme toggle, live table sort, media, and a real escape hatch — while holding the zero-JS-by-default promise. Six additions, all either compile-time or sharing the one runtime, which moves from ~5.8 KB to **~7.4 KB gzipped**; the CI budget is raised 6 KB → **8 KB** to match (still un-minified, fully commented). No source-level breaking changes: existing `.wd` pages compile and behave identically.
package/README.md CHANGED
@@ -50,9 +50,10 @@ npm run dev # live demo site — the same site that runs darkmown.com
50
50
 
51
51
  ## Commands
52
52
 
53
- - `darkmown init [dir]` scaffolds a new site.
54
- - `darkmown dev` starts the live compiler with browser reload and an in-browser error overlay when a build fails.
55
- - `darkmown build` writes static output to `dist`.
53
+ - `darkmown init [dir] [--template <name>]` scaffolds a new site from a template (`starter`, `blog`, `store`, `dashboard`, `landing`).
54
+ - `darkmown dev` starts the live compiler with browser reload, an in-browser error overlay, and a local runner for `api/*` functions.
55
+ - `darkmown build [--target cloudflare]` writes static output to `dist`.
56
+ - `darkmown deploy <vercel|cloudflare> [--prod]` builds and deploys via the platform CLI.
56
57
  - `darkmown serve` previews the built `dist` locally.
57
58
  - `darkmown version` prints the installed package version.
58
59
  - `darkmown help` prints CLI usage.
@@ -556,6 +557,16 @@ You have { cart } items.
556
557
  - **Shared across tabs.** Change a store in one tab and every other tab on the same site updates live.
557
558
  - **Global by name.** A bare `{ cart }` reads the same store everywhere — stores are never section-scoped.
558
559
  - **Same value grammar as `:state`** — string, number, boolean, `null`, array, or object — and the **same** button actions (`cart += …`, `count++`, `theme = "dark"`, and so on).
560
+ - **Multi-line seeds.** An array or object literal may span several lines for readability — open the `[`/`{` on the `:store`/`:state` line and let it run until it closes:
561
+
562
+ ```wd
563
+ :store rows = [
564
+ {"id": 1, "label": "One"},
565
+ {"id": 2, "label": "Two"}
566
+ ]
567
+ ```
568
+
569
+ The literal must balance with no blank line inside it (a blank line ends the value); an unterminated literal is a compile error. Quote genuinely literal bracket text — `:state tag = "[draft]"`.
559
570
 
560
571
  The declared value is a **seed**: it is used only the first time, when the store is absent from storage. After that the persisted value wins, so visitors keep their data.
561
572
 
@@ -605,6 +616,56 @@ Fetched data and a form live happily on the same page:
605
616
  - `:computed total = items.length * 4` derives state from state with a compile-time-checked expression — names, numbers, arithmetic, comparisons, and list aggregates (`sum`/`avg`/`min`/`max`/`count`). See [Computed values](#computed-values--computed).
606
617
  - `:if item.path` works inside reactive loops for per-row branches, and nests — an inner `:if` resolves after the outer branch and stays reactive.
607
618
 
619
+ ## Backends & deploy
620
+
621
+ Darkmown builds **100% static, CDN-cacheable HTML** — never per-request server rendering. Reactive pages hydrate from data-attributes client-side; dynamic data arrives via `:fetch`. When you need a backend, you don't learn a new syntax — you write a plain serverless function, and *it just comes with wherever you deploy*.
622
+
623
+ A backend endpoint is a plain-JS Web-standard handler in a top-level `api/` directory:
624
+
625
+ ```js
626
+ // api/subscribe.js → /api/subscribe
627
+ export const config = { runtime: "edge" }; // Vercel runs api/ as Edge Functions
628
+
629
+ export default async function (request, context) {
630
+ const { email } = await request.json(); // context.params for /api/users/[id]
631
+ return Response.json({ ok: true, email });
632
+ }
633
+ ```
634
+
635
+ - **One shape, every host.** `export default (request) => Response` is exactly what Vercel Edge, Cloudflare Pages, and Netlify Edge run. `api/users/[id].js` → `/api/users/:id`.
636
+ - **Local parity.** `darkmown dev` runs a local runner, so `:fetch /api/subscribe` and `:form action="/api/subscribe"` behave the same as production before you ever deploy.
637
+ - **Deploy in one command.** `darkmown deploy vercel` (functions run natively) or `darkmown deploy cloudflare` (the build emits a `dist/_worker.js` that routes `/api/*` and serves the rest from `env.ASSETS`). It prints your URL, or the login to run if the platform CLI isn't signed in.
638
+ - **Custom server / remote backend.** Point `:fetch`/`:form` at an absolute `https://…` URL and widen the CSP `connect-src` (and `form-action` for native form POSTs). Darkmown owns no server — it adapts to yours.
639
+
640
+ Templates get you to a running, deployable app fast: `darkmown init shop --template store` ships a cart **and** an `api/checkout.js`; `--template dashboard` ships a `:fetch` view **and** an `api/metrics.js`.
641
+
642
+ ## Interactions — `:slider`, `:sortable`, `:carousel`
643
+
644
+ Rich interactions are **pay-for-what-you-use**: `:sortable`/`:carousel` compile to a tiny `/__wd/behaviors/<name>.js` module injected **only** on pages that use them, budgeted separately from the ≤8 KB core runtime. `:slider` is compile-time only — zero extra JS.
645
+
646
+ ```wd
647
+ :slider volume = 50 min=0 max=100 step=5
648
+ Volume: { volume }
649
+
650
+ :store tasks = ["Draft", "Review", "Ship"]
651
+ @loop tasks into t sortable
652
+ - { t }
653
+ @endloop
654
+
655
+ :carousel autoplay=4000
656
+ ::: slide
657
+ First slide
658
+ :::
659
+ ::: slide
660
+ Second slide
661
+ :::
662
+ :endcarousel
663
+ ```
664
+
665
+ - **`:slider name = v min max step`** renders a range input two-way bound through `:bind`; range values coerce to Number so `:computed`/math see a number. Ships no behavior module.
666
+ - **`:sortable`** (a `@loop` clause) drag-reorders the underlying `:state`/`:store` list via Pointer Events (mouse + touch), with full keyboard support (Arrow Up/Down on a focused row, screen-reader instructions + a live "Moved to position N of M" announcement), rewriting the list through the public `window.wd` API so the keyed loop repaints. Valid only on a plain reactive loop (no `where`/`sort`/`reverse`/`offset`/`limit`).
667
+ - **`:carousel [autoplay=N]`** treats **each direct child block as one slide** (wrap each in its own block, e.g. `::: slide`, and size it in your skin), using native CSS scroll-snap (touch swipe is free) plus prev/next buttons, dot navigation, and mouse drag. `autoplay` is suppressed under `prefers-reduced-motion`.
668
+
608
669
  ## Inline attributes
609
670
 
610
671
  A trailing `{.class .class #id}` attaches classes / an id to the inline element directly before it — most often to style a link as a button without a wrapper:
package/docs/cli.md CHANGED
@@ -20,18 +20,21 @@ Prints the command summary, authoring directories, and common directive syntax.
20
20
 
21
21
  Prints the installed package version.
22
22
 
23
- ### `darkmown init [dir]`
23
+ ### `darkmown init [dir] [--template <name>]`
24
24
 
25
- Creates a minimal Darkmown site with:
25
+ Scaffolds a Darkmown site from a template (default: `starter`). Available templates:
26
26
 
27
- - `package.json` (pins the current `@zvndev/darkmown` version)
28
- - `site/pages/index.wd`
29
- - `site/pages/index.skin`
30
- - `site/pages/about.md`
31
- - `site/_/nav.wd`
32
- - `README.md`
27
+ - **`starter`** — the minimal counter + todo-loop site.
28
+ - **`blog`** — markdown posts + a `.wd` index that loops a JSON manifest.
29
+ - **`store`** — product grid, a shared `:store` cart, and a checkout that posts to `api/checkout.js`.
30
+ - **`dashboard`** — stat cards that `:fetch` from `api/metrics.js`.
31
+ - **`landing`** — a marketing one-pager with a `:carousel`.
33
32
 
34
- Existing files are not overwritten. The scaffolded `package.json` is private by default, includes `dev`, `build`, and `preview` scripts, and names the app after the target directory.
33
+ ```sh
34
+ darkmown init my-site --template store
35
+ ```
36
+
37
+ Existing files are never overwritten. The generated `package.json` is private, sets `"type": "module"` (so `api/*.js` import as ESM), includes `dev`/`build`/`preview` scripts, and names the app after the target directory.
35
38
 
36
39
  After scaffolding:
37
40
 
@@ -52,17 +55,28 @@ Runs a live compiler:
52
55
  - injects `/__wd/dev-client.js`
53
56
  - reloads the browser through `/__wd/dev-events`
54
57
 
55
- ### `darkmown build`
58
+ ### `darkmown build [--target cloudflare]`
59
+
60
+ Compiles `site/pages` into `dist`. The output is always 100% static HTML.
56
61
 
57
- Compiles `site/pages` into `dist`.
62
+ Static pages do not receive `/__wd/runtime.js`; reactive pages do. Pay-for-what-you-use behavior modules (`:sortable`, `:carousel`) emit to `dist/__wd/behaviors/` only on the pages that use them.
58
63
 
59
- Static pages do not receive `/__wd/runtime.js`; reactive pages do.
64
+ `--target cloudflare` additionally emits `dist/_worker.js` — a Cloudflare Pages worker that routes `/api/*` to the project's `api/*.js` functions and serves everything else from `env.ASSETS`. The default target leaves `api/` for Vercel to run natively. `darkmown deploy` sets the right target automatically.
60
65
 
61
66
  When a `.md` file contains `.wd` syntax (directives, includes, loops), the build prints a hint suggesting a rename to `.wd` — the syntax stays plain text in `.md` by design.
62
67
 
68
+ ### `darkmown deploy <vercel|cloudflare> [--prod]`
69
+
70
+ Builds (target-aware) and deploys to the platform by wrapping its CLI:
71
+
72
+ - **`vercel`** — writes a `vercel.json` if absent, then runs `npx vercel deploy` (`--prod` to promote). `api/` functions run natively as Edge Functions.
73
+ - **`cloudflare`** — builds the `dist/_worker.js`, then runs `npx wrangler pages deploy dist --project-name <name>`.
74
+
75
+ The deploy URL is printed when the CLI reports it. If the platform CLI isn't signed in, the command surfaces the login to run (`npx vercel login` / `npx wrangler login`) — in a Claude Code session you can run it inline with the `!` prefix — then re-run the deploy.
76
+
63
77
  ### `darkmown serve`
64
78
 
65
- Serves the already-built `dist` directory for local preview. Run `darkmown build` first.
79
+ Serves the already-built `dist` directory for local preview (static only — use `darkmown dev` to exercise `api/*` locally). Run `darkmown build` first.
66
80
 
67
81
  ## Smoke checks
68
82
 
@@ -74,8 +88,24 @@ npm run smoke
74
88
 
75
89
  The smoke script packs the local tarball, installs the packed CLI in a temporary driver project, scaffolds a consumer app through that installed bin, installs the same tarball into the app, builds it, verifies the reactive home route, and verifies the plain `.md` about route stays zero-JS.
76
90
 
77
- ## Deployment notes
91
+ ## Backends (`api/`)
92
+
93
+ Backend endpoints are plain-JS Web-standard handlers in a top-level `api/` directory — the same shape on every host, and the only thing you write for a server:
94
+
95
+ ```js
96
+ // api/subscribe.js → /api/subscribe
97
+ export const config = { runtime: "edge" }; // Vercel: run as an Edge Function
98
+
99
+ export default async function (request, context) {
100
+ const { email } = await request.json();
101
+ return Response.json({ ok: true, email });
102
+ }
103
+ ```
104
+
105
+ `api/users/[id].js` maps to `/api/users/:id` (`context.params.id`). These run in `darkmown dev` (local runner), on **Vercel** natively, and on **Cloudflare Pages** via the `dist/_worker.js` that `darkmown deploy cloudflare` emits. `:fetch`/`:form` point at `/api/*` with no extra config. Need a Node-only API on Vercel? Drop `export const config` and use `(req, res)` — note local-dev parity is then lost for that function.
106
+
107
+ **Custom server / remote backend.** Point `:fetch`/`:form` at an absolute URL (`https://api.example.com/…`) and widen the CSP `connect-src` (and `form-action` for native form POSTs) in `vercel.json` / `dist/_headers` / your serve config. Darkmown still ships static; only the requests leave your origin.
78
108
 
79
- ### Cloudflare Pages
109
+ ### Cloudflare Pages note
80
110
 
81
- `wrangler.toml` builds and deploys the static `dist/` output for Cloudflare Pages. That configuration does **not** provide the demo `/__wd/echo` endpoint used by server-backed form examples; Vercel handles that route separately through `vercel.json` and `api/echo.js`. Add a Cloudflare Pages Function before advertising that demo endpoint on Cloudflare.
111
+ Cloudflare advanced mode (`dist/_worker.js`) does not run a bundler, so an `api/` handler deployed to Cloudflare must be dependency-free (or pre-bundled). The same source still runs on Vercel (which bundles) and in `darkmown dev`.
@@ -41,7 +41,7 @@ This audit compares the current Darkmown implementation with the original MARROW
41
41
 
42
42
  ## Stage 5 additions (2026-06-11, same day)
43
43
 
44
- - **Adapter-style Tier 2 (decided):** Darkmown does not own a server. `:form action="/url" into reply` posts urlencoded via fetch and lands the JSON reply in state, degrading to a plain native POST without JS. Sessions ride on `:fetch` plus ordinary cookies against any backend. A first-party `site/api/` runtime stays open as a future option if a real project demands it.
44
+ - **Adapter-style Tier 2 (decided):** Darkmown does not own a server. `:form action="/url" into reply` posts urlencoded via fetch and lands the JSON reply in state, degrading to a plain native POST without JS. Sessions ride on `:fetch` plus ordinary cookies against any backend.
45
45
  - **Dev error overlay:** failed rebuilds render the compiler error in the browser; the overlay clears on the next good build.
46
46
  - **`darkmown serve`:** local preview of the built `dist`, no dev client injected.
47
47
  - **Packaging:** v0.3.0 introduced package metadata and pack tests; the current public package is MIT licensed and scaffolded projects pin the live Darkmown version.
@@ -93,6 +93,13 @@ The post-8/10-review sprint: one reactive-loop capability, a deploy-hardening la
93
93
  - **Dark-mode theming — `tokens dark`:** a `.skin` file may add a `tokens dark` block alongside its base `tokens`; the dark tokens compile to `@media (prefers-color-scheme: dark) :root { … }`, overriding the matching base tokens under the visitor's OS dark preference. Rules keep referencing the same `$name` tokens, so dark mode is a palette swap with **zero JavaScript** — consistent with the existing zero-JS `.skin` output. A skin with no `tokens dark` block is unchanged.
94
94
  - **Test backfill + compile-error line numbers:** the sprint backfilled runtime/compiler tests for the new surface, and compile errors now carry the offending **line number** alongside the file path and corrective suggestion. **Honest caveat:** this entry documents the intended sprint scope; the exact assertion count and the precise error-message format are owned by the test/compiler tracks and were not verified from this docs track.
95
95
 
96
+ ## Stage 17 additions (2026-06-27, v1.1.0)
97
+
98
+ Follow-up hardening of the two rough edges flagged at the 1.0 release. No runtime change.
99
+
100
+ - **Multi-line `:state`/`:store`/`:theme` JSON values:** an array/object seed may span lines. `body.js` detects an unbalanced `[`/`{` after `=` on the directive line and gathers contiguous continuation lines (stopping at a blank line, which delimits the literal) until the brackets balance — string- and escape-aware so a `]`/`}` or `\"` inside a JSON string is not miscounted — then collapses them to one line the existing single-line handler parses. 1.0 turned the silent `"["` corruption into a compile error; 1.1 makes the readable multi-line form work. An unterminated literal stays a clear compile error; quote literal bracket text. **Honest caveat:** no blank lines inside the literal (a blank line ends the scan), and the opening bracket must be on the directive line.
101
+ - **Single source of truth for the runtime-size budget (internal):** the gzip ceiling lives only in `.size-snapshot.json` (`runtime.budget`); `scripts/size-check.mjs` reads it and `package.json` + `ci.yml` delegate to that script. A `tests/size.test.js` drift guard fails if any config reintroduces a hardcoded budget or inline gate — closing the class of bug that failed the 1.0 PR (a stale `6144` left in `ci.yml`).
102
+
96
103
  ## Stage 16 additions (2026-06-27, v1.0.0)
97
104
 
98
105
  The 1.0 release: a value layer, a time primitive, an explicit theme toggle, reactive table sort, media, and a first-class escape hatch — the gaps an audit flagged as "drop to raw HTML/JS." All compile-time or shared-runtime; the runtime moved from ~5.8 KB to ~7.4 KB gzipped and the CI budget was raised 6 KB → **8 KB** to match (still un-minified). No source-level breaking changes.
@@ -127,13 +134,19 @@ Four progressive-disclosure capabilities plus an assets and a security change. T
127
134
  - **Page-colocated static assets:** any non-page file under `site/pages/` (anything but `.md`/`.wd` routes and colocated `.skin`/`.js`) is copied to `dist/` at its path (`site/pages/logo.svg` → `dist/logo.svg`). The shelf (`site/_/` → `/__wd/media`, `/__wd/data`) is unchanged; a name clash with an emitted route is skipped with a warning. As of v0.14.1, hidden path segments (`.`, `-`, `_`) and symlinked page assets are skipped. `.avif` was added to the served content-types.
128
135
  - **`:fetch` URL scheme hardening (security):** `:fetch`/`refresh=` URLs are validated at compile time — relative, `http(s)://`, or a leading `{ state }` interpolation are allowed; protocol-relative `//host` and non-http(s) schemes (`file:`, `data:`, `javascript:`, …) are rejected with a corrective hint. **This is scheme validation, not a host allowlist** — SSRF to arbitrary http(s) hosts remains the author's responsibility (see the trust boundary).
129
136
 
137
+ ## 1.2.0 additions — backends, deploy & interactions (2026-06-27)
138
+
139
+ - **Serverless `api/` model:** backend endpoints are plain-JS Web-standard handlers — `export default (request, context) => Response` — in a top-level `api/` directory (`api/echo.js` → `/api/echo`; `api/users/[id].js` → `/api/users/:id` with `context.params.id`). Darkmown owns no server; it *adapts* to the host. The build stays 100% static. On Vercel the `api/` functions run natively (Edge); `darkmown deploy cloudflare` (or `build --target cloudflare`) emits `dist/_worker.js` that routes `/api/*` to the same functions and serves everything else from `env.ASSETS`. `darkmown dev` runs a local runner so `/api/*` behaves identically before deploy. No `.wd` backend DSL — minimal new learning, zero lock-in. `form-action 'self'` was added to both CSP variants (and `vercel.json`) so a native `:form action="/api/…"` POST is allowed.
140
+ - **Templates + deploy CLI:** `darkmown init --template <starter|blog|store|dashboard|landing>` scaffolds from in-package templates (each a real, deployable site; `store`/`dashboard` ship an example `api/` function). `darkmown deploy <vercel|cloudflare> [--prod]` writes platform config, runs a target-aware build, and wraps the platform CLI (`npx vercel` / `npx wrangler pages deploy`), printing the deploy URL; an unauthenticated CLI surfaces a login hint.
141
+ - **Interaction behaviors (pay-for-what-you-use):** `:slider name = v min max step` is compile-time only — a range input bound through `:bind` (range values coerce to Number), ships **no** JS module. `:sortable` (a `@loop` clause) and `:carousel` compile to tiny `/__wd/behaviors/<name>.js` modules injected **only** on pages that use them, built on the public `window.wd` API. They are budgeted **separately** from the 8 KB core runtime (each in `.size-snapshot.json`), so core stays untaxed. `:sortable` requires a plain reactive `:state`/`:store` loop (no where/sort/reverse/offset/limit) and reorders via Pointer Events + keyboard; `:carousel` uses native CSS scroll-snap plus prev/next/dots/autoplay/mouse-drag.
142
+
130
143
  ## Demo-only directives (not part of the public surface)
131
144
 
132
- The compiler recognizes three directives — `:note`, `:try`, and `:sprint` — that exist **only to power the demo site** (darkmown.com). They are **not** part of the supported public directive set, are not documented for authors, and may change or be removed without notice. Do not build pages that depend on them. The supported public directives are the ones catalogued above: `:state`, `:store`, `:computed`, `:effect`, `:every`, `:theme`, `:button`, `:bind`, `:if`/`:else if`/`:else`/`:endif`, `:fetch` (with `headers`/`refresh`), `:form`/`:input`/`:submit`, `:video`/`:audio`/`:embed`, `@loop`/`@empty`/`@endloop` (with `where`, `sort by` — including reactive `{ state }` keys —, `reverse`, `offset`, `limit`), `@include`, and `::: section` (with reactive `.class when`). Interpolation supports **format pipes** (`{ value | name:arg }`) and `:computed`/list-pipe **aggregates** (`sum`/`avg`/`min`/`max`/`count`). The runtime escape hatch is `window.wd` (`get`/`set`/`subscribe`/`state`/`render`).
145
+ The compiler recognizes three directives — `:note`, `:try`, and `:sprint` — that exist **only to power the demo site** (darkmown.com). They are **not** part of the supported public directive set, are not documented for authors, and may change or be removed without notice. Do not build pages that depend on them. The supported public directives are the ones catalogued above: `:state`, `:store`, `:computed`, `:effect`, `:every`, `:theme`, `:button`, `:bind`, `:if`/`:else if`/`:else`/`:endif`, `:fetch` (with `headers`/`refresh`), `:form`/`:input`/`:submit`, `:slider`, `:carousel`, `:video`/`:audio`/`:embed`, `@loop`/`@empty`/`@endloop` (with `where`, `sort by` — including reactive `{ state }` keys —, `reverse`, `offset`, `limit`, `sortable`), `@include`, and `::: section` (with reactive `.class when`). Interpolation supports **format pipes** (`{ value | name:arg }`) and `:computed`/list-pipe **aggregates** (`sum`/`avg`/`min`/`max`/`count`). The runtime escape hatch is `window.wd` (`get`/`set`/`subscribe`/`state`/`render`).
133
146
 
134
147
  ## Still missing from the full vision
135
148
 
136
- - First-party server runtime (`site/api/`), HTML-fragment `swap` semantics, and cart server sync — parked behind the adapter decision until a real project needs them.
149
+ - HTML-fragment `swap` semantics and cart server sync — still parked. (The serverless `api/` model shipped in 1.2; a `.wd` backend DSL remains a deliberate non-goal.)
137
150
  - **View transitions:** named/typed transitions (`view-transition-name`, per-element morphs) and `@starting-style` emission, on top of the directional fade + speculation-rules prerender shipped in 0.18.0 (see Stage 4 / Stage 14).
138
151
  - Richer diagnostics and package-consumer QA in regular CI. Editor tooling exists as a source-installable VS Code extension; public Marketplace/Open VSX distribution remains a launch/distribution follow-up.
139
152
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zvndev/darkmown",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -18,7 +18,7 @@
18
18
  "types": "./types/index.d.ts",
19
19
  "scripts": {
20
20
  "build": "node src/cli.js build",
21
- "runtime:size": "node -e \"const fs=require('fs'),zlib=require('zlib'); const size=zlib.gzipSync(fs.readFileSync('dist/__wd/runtime.js')).length; console.log('shipped runtime gzipped: '+size+' bytes (budget: 8192)'); if (size >= 8192) process.exit(1)\"",
21
+ "runtime:size": "node scripts/size-check.mjs",
22
22
  "dev": "node src/cli.js dev",
23
23
  "test": "node --test tests/*.test.js",
24
24
  "test:cov": "node scripts/test-coverage-gate.mjs 100",
@@ -2,6 +2,8 @@
2
2
  { "id": "loops", "title": "Nested `:if` inside loop rows", "shipped": true, "blocked": false },
3
3
  { "id": "editor", "title": "VS Code syntax highlighting", "shipped": true, "blocked": false },
4
4
  { "id": "filter", "title": "Reactive `@loop where` filtering", "shipped": true, "blocked": false },
5
- { "id": "api", "title": "First-party site/api/ runtime", "shipped": false, "blocked": true },
5
+ { "id": "api", "title": "Serverless `api/` model + local-dev runner", "shipped": true, "blocked": false },
6
+ { "id": "deploy", "title": "`init --template` + `darkmown deploy` CLI", "shipped": true, "blocked": false },
7
+ { "id": "behaviors", "title": "Interaction behaviors (`:slider`/`:sortable`/`:carousel`)", "shipped": true, "blocked": false },
6
8
  { "id": "swap", "title": "HTML-fragment swap responses", "shipped": false, "blocked": false }
7
9
  ]
package/site/pages/app.wd CHANGED
@@ -70,7 +70,7 @@ Nice — you are tracking the whole near-term roadmap.
70
70
 
71
71
  This form posts to the demo echo endpoint and shows the reply. With JavaScript it is a `fetch`; without it, the exact same markup is a native POST. On Vercel/dev the sample endpoint replies; on static-only hosts, point `action=` at your own backend — Darkmown adapts instead of owning one:
72
72
 
73
- :form action="/__wd/echo" into reply
73
+ :form action="/api/echo" into reply
74
74
  :input note placeholder="Tell us what you'd build with this"
75
75
  :submit "Send to the server"
76
76
  :endform
@@ -95,7 +95,7 @@ Submit the form and this sentence reacts. Nothing leaves the page.
95
95
 
96
96
  Darkmown does not own your backend — point a form at any endpoint. With JS, the submit becomes a fetch and the JSON reply lands in state; without JS, the same markup is a plain native POST:
97
97
 
98
- :form action="/__wd/echo" into reply
98
+ :form action="/api/echo" into reply
99
99
  :input message placeholder="Say something to the server"
100
100
  :submit "Send to server"
101
101
  :endform
@@ -110,7 +110,7 @@ The reply will appear here.
110
110
  The request failed: { reply_error }
111
111
  :endif
112
112
 
113
- In `darkmown dev` the echo endpoint is built into the dev server; on darkmown.com it is a real serverless function behind the same URL. On static-only hosts such as a plain Cloudflare Pages deploy, replace `action="/__wd/echo"` with your own API endpoint. Either way, Darkmown itself stays static — sessions ride on `:fetch` plus ordinary cookies against your real API.
113
+ In `darkmown dev` the echo endpoint is built into the dev server; on darkmown.com it is a real serverless function behind the same URL. On static-only hosts such as a plain Cloudflare Pages deploy, replace `action="/api/echo"` with your own API endpoint. Either way, Darkmown itself stays static — sessions ride on `:fetch` plus ordinary cookies against your real API.
114
114
 
115
115
  ## Persistent state survives reload
116
116
 
@@ -349,6 +349,7 @@ You have { cart } items.
349
349
  - Saved to localStorage under `wd:store:<name>` and reloaded on the next visit; changes sync live to other open tabs.
350
350
  - The declared value is a **seed**, used only the first time — afterward the persisted value wins.
351
351
  - Same value grammar and the same button actions as `:state`.
352
+ - An array/object seed may span **multiple lines** for readability — open the `[`/`{` on the directive line and let it run until it closes (no blank line inside; an unterminated literal is a compile error). Quote literal bracket text: `:state tag = "[draft]"`.
352
353
  - Add `ephemeral` for an in-memory store that resets on reload: `:store sidebarOpen = false ephemeral`.
353
354
 
354
355
  ## Forms and persistence
@@ -361,6 +362,27 @@ You have { cart } items.
361
362
 
362
363
  See it all live on the [Data & Forms page](/data/).
363
364
 
365
+ ## Backends & deploy
366
+
367
+ Darkmown builds 100% static HTML — never per-request rendering. When you need a server, you write a plain serverless function; there is no Darkmown backend syntax to learn.
368
+
369
+ - A backend endpoint is a Web-standard handler in a top-level `api/` directory: `export default (request, context) => Response`. `api/echo.js` → `/api/echo`; `api/users/[id].js` → `/api/users/:id`.
370
+ - One shape runs everywhere: `darkmown dev` (local runner), Vercel (native Edge Functions), and Cloudflare Pages (the build emits a `dist/_worker.js`). `:fetch`/`:form` point at `/api/*` with no extra config.
371
+ - `darkmown deploy vercel` or `darkmown deploy cloudflare` builds and ships in one command, printing your URL.
372
+ - For a remote/custom backend, point `:fetch`/`:form` at an absolute URL and widen the CSP `connect-src` / `form-action`. Darkmown owns no server — it adapts to yours.
373
+
374
+ `darkmown init shop --template store` scaffolds a cart **and** its `api/checkout.js`; `--template dashboard` ships a `:fetch` view **and** `api/metrics.js`.
375
+
376
+ ## Interactions — `:slider`, `:sortable`, `:carousel`
377
+
378
+ Pay-for-what-you-use: `:sortable`/`:carousel` compile to a tiny `/__wd/behaviors/*.js` module loaded only where used, budgeted separately from the ≤8 KB core runtime. `:slider` is compile-time only.
379
+
380
+ - `:slider name = v min=0 max=100 step=5` — a range input bound through `:bind`; range values coerce to Number.
381
+ - `:sortable` (a `@loop` clause) — drag-to-reorder a `:state`/`:store` list via Pointer Events, with keyboard (Arrow Up/Down) + screen-reader announcements; valid on a plain reactive loop only.
382
+ - `:carousel [autoplay=N]` — each direct child block is one slide (wrap each in its own block, e.g. `::: slide`); native scroll-snap plus prev/next, dots, and mouse drag. Autoplay respects `prefers-reduced-motion`.
383
+
384
+ See all three live on the [Interactions page](/interactions/).
385
+
364
386
  ## Theme toggle — `:theme`
365
387
 
366
388
  For an explicit light/dark switch alongside the OS preference, declare `:theme` once and drive it with buttons. It registers a durable `theme` store and reflects onto `<html data-theme>`:
@@ -106,7 +106,7 @@ Tax (8%)
106
106
 
107
107
  This is a real form. With the runtime it posts over `fetch` and shows the reply inline; with JavaScript off, the identical markup degrades to a native POST. The demo endpoint simply echoes your order back.
108
108
 
109
- :form action="/__wd/echo" into receipt
109
+ :form action="/api/echo" into receipt
110
110
  :input name placeholder="Full name" required
111
111
  :input email type=email placeholder="you@studio.com" required
112
112
  :select shipping
@@ -172,7 +172,7 @@ Total with tax — **{ total | money }**
172
172
 
173
173
  :button "Clear basket" -> cart clear
174
174
 
175
- :form action="/__wd/echo" into receipt
175
+ :form action="/api/echo" into receipt
176
176
  :input name placeholder="Full name" required
177
177
  :input email type=email placeholder="you@studio.com" required
178
178
  :select shipping
@@ -247,7 +247,7 @@ A month-to-month list price would run **${ annual }/yr**. Choose yearly at check
247
247
  Drop in your details and we'll spin up your **${ total }/mo** workspace — no charge for 14 days.
248
248
 
249
249
  ::: div .checkout-card
250
- :form action="/__wd/echo" into receipt
250
+ :form action="/api/echo" into receipt
251
251
  :input name placeholder="Your name"
252
252
  :input email placeholder="Work email"
253
253
  :submit "Start 14-day trial"
@@ -0,0 +1,113 @@
1
+ main.app-interactions
2
+ max-width 760px
3
+ margin 0 auto
4
+ padding 2.5rem 1.5rem
5
+
6
+ .ix-eyebrow
7
+ text-transform uppercase
8
+ letter-spacing .08em
9
+ font-size .8rem
10
+ opacity .7
11
+
12
+ .ix-tagline
13
+ font-size 1.05rem
14
+ opacity .85
15
+ margin-top .5rem
16
+
17
+ .ix-panel
18
+ margin 1.5rem 0
19
+ padding 1.5rem
20
+
21
+ .ix-panel h2
22
+ margin-top 0
23
+
24
+ .ix-note
25
+ margin-top 1rem
26
+ font-size .9rem
27
+ opacity .7
28
+
29
+ .readout
30
+ margin-top .75rem
31
+ font-size 1.1rem
32
+
33
+ input[type=range]
34
+ width 100%
35
+ accent-color $accent
36
+
37
+ .sortlist .row
38
+ display flex
39
+ align-items center
40
+ gap .6rem
41
+ padding .7rem .9rem
42
+ margin .4rem 0
43
+ border 1px solid $line
44
+ radius 10px
45
+ background $paper
46
+ cursor grab
47
+ user-select none
48
+
49
+ .sortlist .row[data-wd-dragging]
50
+ opacity .6
51
+ cursor grabbing
52
+
53
+ .sortlist .row:focus-visible
54
+ outline 2px solid $accent
55
+ outline-offset 2px
56
+
57
+ .grip
58
+ color $muted
59
+ font-size 1.1rem
60
+
61
+ .wd-carousel
62
+ position relative
63
+ margin-top .5rem
64
+
65
+ .wd-carousel-track
66
+ display flex
67
+ overflow-x auto
68
+ scroll-snap-type x mandatory
69
+ scroll-behavior smooth
70
+ gap 1rem
71
+
72
+ .wd-carousel-track .slide
73
+ flex 0 0 100%
74
+ scroll-snap-align start
75
+ border 1px solid $line
76
+ radius 12px
77
+ padding 1.75rem
78
+ background $paper
79
+
80
+ .wd-carousel-prev, .wd-carousel-next
81
+ position absolute
82
+ top 40%
83
+ border 1px solid $line
84
+ background $paper
85
+ color $ink
86
+ radius 999px
87
+ width 2.25rem
88
+ height 2.25rem
89
+ cursor pointer
90
+
91
+ .wd-carousel-prev
92
+ left -.5rem
93
+
94
+ .wd-carousel-next
95
+ right -.5rem
96
+
97
+ .wd-carousel-dots
98
+ display flex
99
+ justify-content center
100
+ gap .5rem
101
+ margin-top .75rem
102
+
103
+ .wd-carousel-dot
104
+ width .6rem
105
+ height .6rem
106
+ padding 0
107
+ border 0
108
+ radius 999px
109
+ background $line
110
+ cursor pointer
111
+
112
+ .wd-carousel-dot[aria-current=true]
113
+ background $accent
@@ -0,0 +1,86 @@
1
+ ---
2
+ title: Interactions — sliders, drag & carousels in Markdown
3
+ description: :slider, :sortable, and :carousel — rich interactions that compile to tiny pay-for-what-you-use modules, declared in plain Markdown. Sliders ship zero JS; sortable and carousel each load one small module, only on the pages that use them.
4
+ image: https://darkmown.com/__wd/media/og-showcase.png
5
+ transitions: true
6
+ ---
7
+
8
+ @include /nav.wd
9
+ @include /base.wd
10
+
11
+ <main class="wide app-interactions">
12
+
13
+ ::: section #interactions
14
+
15
+ ::: ix-head
16
+ ::: eyebrow .ix-eyebrow
17
+ Pay-for-what-you-use interactions
18
+ :::
19
+
20
+ # *Interactions*
21
+
22
+ ::: ix-tagline
23
+ A range slider, drag-to-reorder, and a carousel — all declared in Markdown. The
24
+ slider is compile-time (zero JavaScript). `:sortable` and `:carousel` each load
25
+ one tiny module from `/__wd/behaviors/`, **only** on this page.
26
+ :::
27
+ :::
28
+
29
+ ::: card .ix-panel
30
+ ## Slider
31
+
32
+ :slider volume = 60 min=0 max=100 step=5
33
+
34
+ ::: readout
35
+ Volume: **{ volume }** / 100
36
+ :::
37
+
38
+ ::: ix-note
39
+ `:slider` renders a bound range input. Range values coerce to a Number, so
40
+ `{ volume }` and any `:computed` math see `60`, not `"60"`.
41
+ :::
42
+ :::
43
+
44
+ ::: card .ix-panel
45
+ ## Drag to reorder
46
+
47
+ :store tasks = ["Sketch the idea", "Draft the page", "Review with the team", "Ship it"]
48
+
49
+ ::: sortlist
50
+ @loop tasks into t sortable
51
+ ::: row
52
+ <span class="grip" aria-hidden="true">⠿</span> { t }
53
+ :::
54
+ @endloop
55
+ :::
56
+
57
+ ::: ix-note
58
+ Drag a row to reorder, or focus one and press ↑ / ↓. The order is rewritten on
59
+ the underlying `:store` through `window.wd`, so the keyed loop repaints.
60
+ :::
61
+ :::
62
+
63
+ ::: card .ix-panel
64
+ ## Carousel
65
+
66
+ :carousel
67
+ ::: slide
68
+ ### Native swipe
69
+ CSS scroll-snap does the swiping — touch works with zero JavaScript.
70
+ :::
71
+ ::: slide
72
+ ### Controls, added
73
+ The module adds prev/next buttons, dot navigation, and mouse drag.
74
+ :::
75
+ ::: slide
76
+ ### Loaded on demand
77
+ `carousel.js` ships only because this page uses `:carousel`.
78
+ :::
79
+ :endcarousel
80
+ :::
81
+
82
+ :::
83
+
84
+ </main>
85
+
86
+ @include /footer.wd
@@ -13,7 +13,16 @@ transitions: true
13
13
  ::: section #ledger
14
14
 
15
15
  :theme
16
- :store expenses = [{"id":1,"date":"2026-06-22","merchant":"Aurora Studio","category":"Software","amount":149},{"id":2,"date":"2026-06-20","merchant":"Briza Coffee","category":"Meals","amount":18.4},{"id":3,"date":"2026-06-19","merchant":"Cobalt Cloud","category":"Hosting","amount":312.5},{"id":4,"date":"2026-06-18","merchant":"Dune Print Co","category":"Office","amount":64},{"id":5,"date":"2026-06-16","merchant":"Aurora Studio","category":"Software","amount":29},{"id":6,"date":"2026-06-15","merchant":"Ember Transit","category":"Travel","amount":212.75},{"id":7,"date":"2026-06-13","merchant":"Briza Coffee","category":"Meals","amount":11.2},{"id":8,"date":"2026-06-11","merchant":"Cobalt Cloud","category":"Hosting","amount":312.5}]
16
+ :store expenses = [
17
+ {"id":1,"date":"2026-06-22","merchant":"Aurora Studio","category":"Software","amount":149},
18
+ {"id":2,"date":"2026-06-20","merchant":"Briza Coffee","category":"Meals","amount":18.4},
19
+ {"id":3,"date":"2026-06-19","merchant":"Cobalt Cloud","category":"Hosting","amount":312.5},
20
+ {"id":4,"date":"2026-06-18","merchant":"Dune Print Co","category":"Office","amount":64},
21
+ {"id":5,"date":"2026-06-16","merchant":"Aurora Studio","category":"Software","amount":29},
22
+ {"id":6,"date":"2026-06-15","merchant":"Ember Transit","category":"Travel","amount":212.75},
23
+ {"id":7,"date":"2026-06-13","merchant":"Briza Coffee","category":"Meals","amount":11.2},
24
+ {"id":8,"date":"2026-06-11","merchant":"Cobalt Cloud","category":"Hosting","amount":312.5}
25
+ ]
17
26
  :state q = ""
18
27
  :state sortKey = "amount"
19
28
  :state sortDir = "desc"