@zvndev/darkmown 2.6.1-next.11.g82bb8d7 → 2.7.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/AGENTS.md +392 -77
- package/CHANGELOG.md +47 -0
- package/README.md +316 -49
- package/docs/cli.md +51 -17
- package/docs/spec-alignment.md +25 -4
- package/grammar/wd-directives.gbnf +13 -5
- package/package.json +7 -3
- package/site/_/base.skin +1 -1
- package/site/_/footer.wd +1 -1
- package/site/pages/api-errors/index.wd +81 -0
- package/site/pages/app.wd +1 -1
- package/site/pages/attr-binding/index.wd +84 -0
- package/site/pages/blog/darkmown-2-7-0.md +176 -0
- package/site/pages/blog/darkmown-2-7-0.skin +127 -0
- package/site/pages/blog/no-unsafe-eval.md +66 -0
- package/site/pages/blog/no-unsafe-eval.skin +127 -0
- package/site/pages/blog/zero-js-by-default.md +1 -1
- package/site/pages/branch-state/index.wd +68 -0
- package/site/pages/docs/index.wd +320 -19
- package/site/pages/index.wd +3 -1
- package/site/pages/playground/index.js +63 -56
- package/site/pages/pulse/index.wd +1 -1
- package/site/pages/settings/index.wd +97 -0
- package/site/pages/showcase/index.wd +37 -1
- package/site/pages/upload/index.wd +63 -0
- package/site/pages/url-state/index.wd +70 -0
- package/src/builder.js +222 -34
- package/src/catalog.js +217 -21
- package/src/cli.js +6 -3
- package/src/compiler/actions.js +80 -12
- package/src/compiler/body.js +104 -28
- package/src/compiler/collections.js +29 -13
- package/src/compiler/context.js +9 -2
- package/src/compiler/expr-ast.js +14 -0
- package/src/compiler/fetch.js +8 -4
- package/src/compiler/forms.js +133 -14
- package/src/compiler/frontmatter.js +28 -7
- package/src/compiler/includes.js +46 -6
- package/src/compiler/interpolation.js +56 -1
- package/src/compiler/loops.js +489 -35
- package/src/compiler/markdown.js +457 -44
- package/src/compiler/page.js +8 -3
- package/src/compiler/predicates.js +84 -14
- package/src/compiler/state.js +167 -18
- package/src/compiler/structure.js +159 -29
- package/src/errors.js +111 -3
- package/src/feeds.js +140 -7
- package/src/grammar.js +53 -10
- package/src/runtime.js +509 -130
- package/src/runtime.min.js +2 -0
- package/src/runtime.min.js.map +1 -0
- package/src/statics.js +46 -2
- package/src/templates/blog/README.md +2 -0
- package/src/templates/dashboard/README.md +2 -0
- package/src/templates/landing/README.md +2 -0
- package/src/templates/starter/README.md +1 -1
- package/src/templates/store/README.md +2 -0
- package/src/tools/validate.js +14 -4
- package/types/builder.d.ts +6 -2
- package/types/catalog.d.ts +12 -0
- package/types/compiler/actions.d.ts +1 -1
- package/types/compiler/body.d.ts +0 -3
- package/types/compiler/forms.d.ts +12 -1
- package/types/compiler/frontmatter.d.ts +8 -1
- package/types/compiler/includes.d.ts +16 -2
- package/types/compiler/interpolation.d.ts +23 -0
- package/types/compiler/loops.d.ts +47 -0
- package/types/compiler/markdown.d.ts +63 -1
- package/types/compiler/predicates.d.ts +7 -1
- package/types/compiler/state.d.ts +8 -2
- package/types/compiler/structure.d.ts +32 -1
- package/types/feeds.d.ts +68 -2
- package/types/statics.d.ts +25 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
---
|
|
2
|
+
html: true
|
|
3
|
+
title: Bound controls: a select, a radio group and a checkbox
|
|
4
|
+
description: Outside a :form, the three field directives bind straight to a declared :state, so a settings panel drives :if regions with no form and no JavaScript of your own.
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
@include /nav.wd
|
|
8
|
+
@include /base.wd
|
|
9
|
+
|
|
10
|
+
<main>
|
|
11
|
+
|
|
12
|
+
<section class="intro">
|
|
13
|
+
<span class="eyebrow">Darkmown 2.7</span>
|
|
14
|
+
|
|
15
|
+
# Bound controls
|
|
16
|
+
|
|
17
|
+
`:select`, `:radio` and `:checkbox` mean two things, and where they sit decides which. Inside a `:form` they are form fields, submitted by name. Outside one they bind to a declared `:state` the way `:bind` and `:slider` already do: move the control and the state changes, change the state and the control moves.
|
|
18
|
+
|
|
19
|
+
</section>
|
|
20
|
+
|
|
21
|
+
<section>
|
|
22
|
+
|
|
23
|
+
## A settings panel
|
|
24
|
+
|
|
25
|
+
:state density = "Comfortable"
|
|
26
|
+
:state theme_pick = "System"
|
|
27
|
+
:state previews = true
|
|
28
|
+
|
|
29
|
+
**Density**
|
|
30
|
+
|
|
31
|
+
:select density
|
|
32
|
+
- Compact
|
|
33
|
+
- Comfortable
|
|
34
|
+
- Spacious
|
|
35
|
+
|
|
36
|
+
**Colour**
|
|
37
|
+
|
|
38
|
+
:radio theme_pick
|
|
39
|
+
- System
|
|
40
|
+
- Light
|
|
41
|
+
- Dark
|
|
42
|
+
|
|
43
|
+
**Previews**
|
|
44
|
+
|
|
45
|
+
:checkbox previews
|
|
46
|
+
- Show image previews
|
|
47
|
+
|
|
48
|
+
</section>
|
|
49
|
+
|
|
50
|
+
<section>
|
|
51
|
+
|
|
52
|
+
## What the panel drives
|
|
53
|
+
|
|
54
|
+
Every region below reads the same state the controls write.
|
|
55
|
+
|
|
56
|
+
:if density == "Compact"
|
|
57
|
+
Compact: rows sit tight together.
|
|
58
|
+
:else if density == "Spacious"
|
|
59
|
+
Spacious: rows get room to breathe.
|
|
60
|
+
:else
|
|
61
|
+
Comfortable: the default spacing.
|
|
62
|
+
:endif
|
|
63
|
+
|
|
64
|
+
:if theme_pick == "System"
|
|
65
|
+
Following the operating system's colour scheme.
|
|
66
|
+
:else
|
|
67
|
+
Forcing **{ theme_pick }** regardless of the system setting.
|
|
68
|
+
:endif
|
|
69
|
+
|
|
70
|
+
:if previews
|
|
71
|
+
Image previews are on.
|
|
72
|
+
:else
|
|
73
|
+
Image previews are off, so the list loads text only.
|
|
74
|
+
:endif
|
|
75
|
+
|
|
76
|
+
Settings as one line each: **{ density }**, **{ theme_pick }**, previews { previews }.
|
|
77
|
+
|
|
78
|
+
:button "Reset to defaults" -> density reset; theme_pick reset; previews reset
|
|
79
|
+
|
|
80
|
+
</section>
|
|
81
|
+
|
|
82
|
+
<section>
|
|
83
|
+
|
|
84
|
+
## The rules
|
|
85
|
+
|
|
86
|
+
- The state has to exist first. A bound field with no matching `:state` is a compile error that names both readings, so a typo can never become a silently inert control.
|
|
87
|
+
- A bound `:radio` group carries the chosen option's text; the group keeps a shared `name` so the browser makes it mutually exclusive.
|
|
88
|
+
- A bound `:checkbox` carries a **boolean**, so it takes exactly one `- Label` line, the label to show beside it. The multi-value checkbox group is the in-form behavior and is unchanged.
|
|
89
|
+
- Inside a `:form` nothing changed at all, even if a `:state` of the same name happens to exist elsewhere on the page.
|
|
90
|
+
|
|
91
|
+
<a class="btn primary" href="/docs/#bound-controls-outside-a-form">Read the docs on bound controls</a>
|
|
92
|
+
|
|
93
|
+
</section>
|
|
94
|
+
|
|
95
|
+
</main>
|
|
96
|
+
|
|
97
|
+
@include /footer.wd
|
|
@@ -96,7 +96,7 @@ Darkmown's whole pitch is weight. A static page ships **zero** framework JavaScr
|
|
|
96
96
|
|
|
97
97
|
::: card .sc-scale
|
|
98
98
|
- **Static Darkmown page** — `0 KB` framework JS. Forever.
|
|
99
|
-
- **Reactive Darkmown page** — one shared `~
|
|
99
|
+
- **Reactive Darkmown page** — one shared `~6.6 KB` gzipped runtime, no per-component bundles.
|
|
100
100
|
- **A typical SPA** — ships a framework runtime in the **~15–60 KB** gzipped range *before* your app code, on every page (Alpine ~15 KB, Vue ~18 KB runtime, React 19 ~59 KB).
|
|
101
101
|
:::
|
|
102
102
|
|
|
@@ -160,6 +160,42 @@ Prefer to see each feature in isolation? These pages tour the building blocks th
|
|
|
160
160
|
[Open →](/markdown/)
|
|
161
161
|
:::
|
|
162
162
|
|
|
163
|
+
::: card .sc-mini
|
|
164
|
+
**Reactive links**: a destination that follows state.
|
|
165
|
+
|
|
166
|
+
[Open →](/attr-binding/)
|
|
167
|
+
:::
|
|
168
|
+
|
|
169
|
+
::: card .sc-mini
|
|
170
|
+
**URL state**: `from-url`, a filter you can share.
|
|
171
|
+
|
|
172
|
+
[Open →](/url-state/)
|
|
173
|
+
:::
|
|
174
|
+
|
|
175
|
+
::: card .sc-mini
|
|
176
|
+
**Bound controls**: select, radio, checkbox, no form.
|
|
177
|
+
|
|
178
|
+
[Open →](/settings/)
|
|
179
|
+
:::
|
|
180
|
+
|
|
181
|
+
::: card .sc-mini
|
|
182
|
+
**Server errors**: what the API actually said.
|
|
183
|
+
|
|
184
|
+
[Open →](/api-errors/)
|
|
185
|
+
:::
|
|
186
|
+
|
|
187
|
+
::: card .sc-mini
|
|
188
|
+
**File upload**: a `:form` that posts multipart.
|
|
189
|
+
|
|
190
|
+
[Open →](/upload/)
|
|
191
|
+
:::
|
|
192
|
+
|
|
193
|
+
::: card .sc-mini
|
|
194
|
+
**Branch state**: a `:state` inside a closed `:if`.
|
|
195
|
+
|
|
196
|
+
[Open →](/branch-state/)
|
|
197
|
+
:::
|
|
198
|
+
|
|
163
199
|
</div>
|
|
164
200
|
|
|
165
201
|
</section>
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
---
|
|
2
|
+
html: true
|
|
3
|
+
title: File upload: a :form that posts multipart
|
|
4
|
+
description: A :form containing a file field submits as multipart FormData instead of a urlencoded body, so the file itself travels. The failure path renders the server's own message and its field-level errors.
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
@include /nav.wd
|
|
8
|
+
@include /base.wd
|
|
9
|
+
|
|
10
|
+
<main>
|
|
11
|
+
|
|
12
|
+
<section class="intro">
|
|
13
|
+
<span class="eyebrow">Darkmown 2.7</span>
|
|
14
|
+
|
|
15
|
+
# File upload
|
|
16
|
+
|
|
17
|
+
A urlencoded body can only carry a file's **name**. A `:form` that contains a file field now declares `enctype="multipart/form-data"` and the runtime posts the `FormData` itself, letting the browser write the multipart boundary. Nothing else about the form changes.
|
|
18
|
+
|
|
19
|
+
</section>
|
|
20
|
+
|
|
21
|
+
<section>
|
|
22
|
+
|
|
23
|
+
## Pick a file and send it
|
|
24
|
+
|
|
25
|
+
:form into reply action="/api/upload/"
|
|
26
|
+
:input photo type=file required
|
|
27
|
+
:input caption placeholder="A caption (optional)"
|
|
28
|
+
:submit "Upload"
|
|
29
|
+
:endform
|
|
30
|
+
|
|
31
|
+
:if reply
|
|
32
|
+
Uploaded **{ reply.name }**, { reply.size } bytes, type `{ reply.type }`.
|
|
33
|
+
:if reply.caption
|
|
34
|
+
Caption: { reply.caption }
|
|
35
|
+
:endif
|
|
36
|
+
:endif
|
|
37
|
+
|
|
38
|
+
:if reply_error
|
|
39
|
+
**{ reply_error }**
|
|
40
|
+
:if reply_error_body
|
|
41
|
+
The server also said which field was wrong: { reply_error_body.fields.photo }
|
|
42
|
+
:endif
|
|
43
|
+
:endif
|
|
44
|
+
|
|
45
|
+
Submit with no file to see the failure path: the endpoint answers `422` with a JSON body, and the page renders the server's own sentence rather than "HTTP 422".
|
|
46
|
+
|
|
47
|
+
</section>
|
|
48
|
+
|
|
49
|
+
<section>
|
|
50
|
+
|
|
51
|
+
## The rules
|
|
52
|
+
|
|
53
|
+
- A file field makes the whole form multipart. The compiler writes the `enctype` for the browser's native submit, and the runtime sends the `FormData` with **no** content-type of its own so the boundary is correct.
|
|
54
|
+
- Bound controls inside the form (a `:bind`, a bound `:select`) carry no `name`, so `FormData` never sees them. On the multipart path they are appended by their state key rather than silently dropped.
|
|
55
|
+
- A file field with `method="get"` is a compile error. A GET request has no body, so only the file's name would ever travel, the kind of failure that looks like it worked.
|
|
56
|
+
|
|
57
|
+
<a class="btn primary" href="/docs/#file-upload">Read the docs on file upload</a>
|
|
58
|
+
|
|
59
|
+
</section>
|
|
60
|
+
|
|
61
|
+
</main>
|
|
62
|
+
|
|
63
|
+
@include /footer.wd
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
---
|
|
2
|
+
html: true
|
|
3
|
+
title: The URL is state: :state … from-url
|
|
4
|
+
description: A filter that survives a reload and travels in a shared link. from-url mirrors a :state into the query string with replaceState, drops the parameter when the value is back at its default, and re-reads on back and forward.
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
@include /nav.wd
|
|
8
|
+
@include /base.wd
|
|
9
|
+
|
|
10
|
+
<main>
|
|
11
|
+
|
|
12
|
+
<section class="intro">
|
|
13
|
+
<span class="eyebrow">Darkmown 2.7</span>
|
|
14
|
+
|
|
15
|
+
# The URL is state
|
|
16
|
+
|
|
17
|
+
A filter nobody can link to is half a feature. Add `from-url` to a `:state` and the value lives in the query string as well as in memory: reload keeps it, a shared link arrives with it applied, and back and forward move through it.
|
|
18
|
+
|
|
19
|
+
</section>
|
|
20
|
+
|
|
21
|
+
<section>
|
|
22
|
+
|
|
23
|
+
## Search and filter
|
|
24
|
+
|
|
25
|
+
:state q = "" from-url
|
|
26
|
+
:state tier = "all" from-url
|
|
27
|
+
|
|
28
|
+
:bind q placeholder="Search products"
|
|
29
|
+
|
|
30
|
+
:radio tier
|
|
31
|
+
- all
|
|
32
|
+
- budget
|
|
33
|
+
- premium
|
|
34
|
+
|
|
35
|
+
:state products = [
|
|
36
|
+
{ "name": "Aurora Lamp", "price": 49, "tier": "budget" },
|
|
37
|
+
{ "name": "Briza Fan", "price": 89, "tier": "budget" },
|
|
38
|
+
{ "name": "Cove Shelf", "price": 240, "tier": "premium" },
|
|
39
|
+
{ "name": "Dune Rug", "price": 310, "tier": "premium" }
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
Tier **{ tier }**, search "{ q }".
|
|
43
|
+
|
|
44
|
+
@loop products into p where p.name contains q
|
|
45
|
+
- **{ p.name }** · { p.price | money } ({ p.tier })
|
|
46
|
+
@empty
|
|
47
|
+
Nothing matches that search.
|
|
48
|
+
@endloop
|
|
49
|
+
|
|
50
|
+
Type in the box and watch the address bar. Then reload the page: the search and the tier come back. Copy the URL into a new tab and it opens on the same view. The tier radio is a second key riding along in the same URL; it is shown above the list rather than wired into the filter, so you can see two `from-url` keys travel together.
|
|
51
|
+
|
|
52
|
+
</section>
|
|
53
|
+
|
|
54
|
+
<section>
|
|
55
|
+
|
|
56
|
+
## The rules
|
|
57
|
+
|
|
58
|
+
- The parameter is named after the state key. A section-scoped key like `cart:items` becomes `cart.items`, so the name is always readable and always unique.
|
|
59
|
+
- A value equal to its declared seed **drops** its parameter, so the default page keeps a clean URL.
|
|
60
|
+
- Writes go through `history.replaceState`, so filtering never fills the back button with one entry per keystroke.
|
|
61
|
+
- Combine it with `persist` and the precedence is URL first, then the stored value, then the seed, so a link you were sent wins over what this browser remembers.
|
|
62
|
+
- `from-url` belongs to `:state` only. A `:store` is shared by every page and every tab, while a query parameter belongs to one page's address, so putting `from-url` on a `:store` is a compile error rather than a guess.
|
|
63
|
+
|
|
64
|
+
<a class="btn primary" href="/docs/#url-as-state-from-url">Read the docs on URL as state</a>
|
|
65
|
+
|
|
66
|
+
</section>
|
|
67
|
+
|
|
68
|
+
</main>
|
|
69
|
+
|
|
70
|
+
@include /footer.wd
|