@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/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
|
|
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
|
-
|
|
25
|
+
Scaffolds a Darkmown site from a template (default: `starter`). Available templates:
|
|
26
26
|
|
|
27
|
-
-
|
|
28
|
-
- `
|
|
29
|
-
- `
|
|
30
|
-
- `
|
|
31
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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`.
|
package/docs/spec-alignment.md
CHANGED
|
@@ -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.
|
|
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
|
-
-
|
|
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.
|
|
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
|
|
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",
|
package/site/_/roadmap.json
CHANGED
|
@@ -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": "
|
|
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="/
|
|
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
|
package/site/pages/data.wd
CHANGED
|
@@ -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="/
|
|
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="/
|
|
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
|
|
package/site/pages/docs/index.wd
CHANGED
|
@@ -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>`:
|
package/site/pages/folio/cart.wd
CHANGED
|
@@ -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="/
|
|
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="/
|
|
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="/
|
|
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 = [
|
|
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"
|