@zvndev/darkmown 1.4.0 → 2.0.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 (106) hide show
  1. package/AGENTS.md +17 -3
  2. package/CHANGELOG.md +38 -0
  3. package/README.md +114 -17
  4. package/SECURITY.md +21 -18
  5. package/docs/cli.md +5 -1
  6. package/docs/spec-alignment.md +31 -4
  7. package/package.json +1 -1
  8. package/site/_/base.skin +239 -18
  9. package/site/_/base.wd +4 -0
  10. package/site/_/feature-card.wd +4 -0
  11. package/site/_/footer.wd +4 -0
  12. package/site/_/nav.wd +5 -1
  13. package/site/_/scoped-promo.wd +4 -0
  14. package/site/_/scoped-stat.wd +4 -0
  15. package/site/pages/404.wd +1 -0
  16. package/site/pages/app.skin +27 -5
  17. package/site/pages/app.wd +1 -0
  18. package/site/pages/blog/_schema.wd +15 -0
  19. package/site/pages/blog/collections-and-pagination.md +38 -0
  20. package/site/pages/blog/collections-and-pagination.skin +127 -0
  21. package/site/pages/blog/drafts-that-never-leak.md +2 -0
  22. package/site/pages/blog/drafts-that-never-leak.skin +127 -0
  23. package/site/pages/blog/index.skin +144 -0
  24. package/site/pages/blog/index.wd +74 -0
  25. package/site/pages/blog/shipping-feeds.md +1 -0
  26. package/site/pages/blog/shipping-feeds.skin +127 -0
  27. package/site/pages/blog/typed-frontmatter.md +45 -0
  28. package/site/pages/blog/typed-frontmatter.skin +127 -0
  29. package/site/pages/blog/unfinished-thoughts.skin +127 -0
  30. package/site/pages/blog/zero-js-by-default.md +31 -0
  31. package/site/pages/blog/zero-js-by-default.skin +127 -0
  32. package/site/pages/carousel/index.wd +1 -0
  33. package/site/pages/compass/index.wd +1 -0
  34. package/site/pages/data.skin +50 -9
  35. package/site/pages/data.wd +3 -0
  36. package/site/pages/docs/-relative-note.wd +4 -0
  37. package/site/pages/docs/index.skin +27 -0
  38. package/site/pages/docs/index.wd +115 -11
  39. package/site/pages/folio/-style.wd +4 -0
  40. package/site/pages/folio/cart.wd +1 -0
  41. package/site/pages/folio/index.wd +1 -0
  42. package/site/pages/forge/index.wd +1 -0
  43. package/site/pages/highlight/index.wd +1 -0
  44. package/site/pages/index.wd +1 -0
  45. package/site/pages/interactions/index.wd +1 -0
  46. package/site/pages/ledger/index.wd +1 -0
  47. package/site/pages/loops.skin +43 -6
  48. package/site/pages/loops.wd +1 -0
  49. package/site/pages/media/index.wd +1 -0
  50. package/site/pages/pulse/index.wd +1 -0
  51. package/site/pages/reactive.skin +38 -6
  52. package/site/pages/reactive.wd +1 -0
  53. package/site/pages/scoped/index.wd +1 -0
  54. package/site/pages/showcase/index.wd +1 -0
  55. package/site/pages/store.skin +43 -6
  56. package/site/pages/store.wd +1 -0
  57. package/site/pages/theme.wd +1 -0
  58. package/src/builder.js +535 -48
  59. package/src/cli.js +96 -18
  60. package/src/compiler/actions.js +303 -0
  61. package/src/compiler/body.js +36 -24
  62. package/src/compiler/collections.js +316 -0
  63. package/src/compiler/context.js +83 -6
  64. package/src/compiler/directives.js +31 -1470
  65. package/src/compiler/fetch.js +159 -0
  66. package/src/compiler/format.js +17 -4
  67. package/src/compiler/forms.js +456 -0
  68. package/src/compiler/frontmatter.js +10 -5
  69. package/src/compiler/includes.js +66 -1
  70. package/src/compiler/index.js +15 -3
  71. package/src/compiler/loops.js +181 -31
  72. package/src/compiler/markdown.js +65 -15
  73. package/src/compiler/media.js +133 -0
  74. package/src/compiler/page.js +78 -41
  75. package/src/compiler/state.js +168 -0
  76. package/src/compiler/structure.js +377 -0
  77. package/src/dev.js +76 -0
  78. package/src/headers.js +56 -17
  79. package/src/router.js +12 -1
  80. package/src/runtime.js +5 -3
  81. package/src/statics.js +63 -12
  82. package/src/templates/blog/README.md +13 -4
  83. package/src/templates/blog/site/_/nav.wd +4 -0
  84. package/src/templates/blog/site/pages/index.skin +87 -0
  85. package/src/templates/blog/site/pages/index.wd +10 -4
  86. package/src/templates/blog/site/pages/posts/_schema.wd +16 -0
  87. package/src/templates/blog/site/pages/posts/hello-darkmown.md +2 -0
  88. package/src/templates/blog/site/pages/posts/hello-darkmown.skin +59 -0
  89. package/src/templates/blog/site/pages/posts/markdown-native.md +9 -4
  90. package/src/templates/blog/site/pages/posts/markdown-native.skin +59 -0
  91. package/src/templates/dashboard/site/_/nav.wd +4 -0
  92. package/src/templates/dashboard/site/pages/index.skin +84 -0
  93. package/src/templates/dashboard/site/pages/index.wd +1 -0
  94. package/src/templates/landing/site/_/nav.wd +4 -0
  95. package/src/templates/landing/site/pages/index.skin +113 -6
  96. package/src/templates/landing/site/pages/index.wd +1 -0
  97. package/src/templates/starter/site/_/nav.wd +4 -0
  98. package/src/templates/starter/site/pages/about.md +18 -1
  99. package/src/templates/starter/site/pages/about.skin +69 -0
  100. package/src/templates/starter/site/pages/index.skin +81 -3
  101. package/src/templates/starter/site/pages/index.wd +1 -0
  102. package/src/templates/store/site/_/nav.wd +4 -0
  103. package/src/templates/store/site/pages/index.skin +93 -11
  104. package/src/templates/store/site/pages/index.wd +1 -0
  105. package/site/pages/blog/index.md +0 -13
  106. package/src/templates/blog/site/_/posts.json +0 -16
@@ -1,4 +1,5 @@
1
1
  ---
2
+ html: true
2
3
  title: Darkmown docs
3
4
  description: Learn Darkmown routing, directives, state, loops, forms, and the static-to-reactive model.
4
5
  transitions: true
@@ -17,6 +18,40 @@ transitions: true
17
18
  Darkmown is a Markdown-native framework for mostly-static sites with tiny reactive islands. Pages are plain files: `.md` stays pure CommonMark, and `.wd` ("whateverdown") is the same Markdown plus first-party directives.
18
19
  </header>
19
20
 
21
+ <nav class="docs-toc" aria-label="On this page">
22
+
23
+ **On this page**
24
+
25
+ - [Install](#install)
26
+ - [The two formats](#the-two-formats)
27
+ - [Routing rules](#routing-rules)
28
+ - [Includes](#includes)
29
+ - [Loops](#loops)
30
+ - [Content collections](#content-collections)
31
+ - [Interpolation](#interpolation)
32
+ - [Frontmatter](#frontmatter)
33
+ - [SEO & feeds](#seo--feeds)
34
+ - [Drafts](#drafts)
35
+ - [Instant navigation](#instant-navigation)
36
+ - [Images](#images)
37
+ - [Sections](#sections)
38
+ - [Reactive directives](#reactive-directives)
39
+ - [Fetching data](#fetching-data)
40
+ - [Global state — `:store`](#global-state--store)
41
+ - [Forms and persistence](#forms-and-persistence)
42
+ - [Backends & deploy](#backends--deploy)
43
+ - [Interactions](#interactions--slider-sortable-carousel)
44
+ - [Theme toggle — `:theme`](#theme-toggle--theme)
45
+ - [Scoped styles — `scoped`](#scoped-styles--scoped)
46
+ - [Media directives](#media--video-audio-embed)
47
+ - [Syntax highlighting](#syntax-highlighting)
48
+ - [Trust boundary](#trust-boundary)
49
+ - [Colocation](#colocation)
50
+ - [Spec status](#spec-status)
51
+ </nav>
52
+
53
+ Every heading on every page gets a stable, GitHub-style `id` at build time (lowercased, punctuation stripped, duplicates deduped), so the links above — and deep links into any Darkmown page — are plain `#` anchors with zero JavaScript.
54
+
20
55
  @include ./-relative-note.wd
21
56
 
22
57
  ## Install
@@ -30,6 +65,10 @@ npm run dev
30
65
 
31
66
  Or add Darkmown to an existing project with `npm install -D @zvndev/darkmown`. The package is scoped; the command it installs is plain `darkmown`.
32
67
 
68
+ For editing, a [VS Code extension](https://github.com/ZVN-DEV/darkmown/tree/master/editors/vscode) gives `.wd` and `.skin` files syntax highlighting, snippets, and folding — install it from a `.vsix` built out of the framework repo (see its `editors/vscode` README; a Marketplace listing is pending).
69
+
70
+ `darkmown dev` rebuilds incrementally: it tracks which routes depend on each file (includes, colocated assets, loop data, collections), so editing one page or include recompiles only the routes it affects — and falls back to a full rebuild whenever a file is added, removed, or renamed. `routes.json`, the sitemap, and the RSS feed are regenerated on every rebuild, so they always describe the whole site.
71
+
33
72
  ## The two formats
34
73
 
35
74
  - `.md` is strict CommonMark. Directives, includes, and `{ bindings }` stay plain text, and the build prints a hint if it spots `.wd` syntax in a `.md` file.
@@ -54,7 +93,7 @@ Or add Darkmown to an existing project with `npm install -D @zvndev/darkmown`. T
54
93
 
55
94
  `@loop <things> into <thing>` is the only loop. The body is normal Markdown (or more directives) and repeats once per row:
56
95
 
57
- ```wd
96
+ ```wd-fragment
58
97
  @loop /features.json into card
59
98
  @include /feature-card.wd
60
99
  @endloop
@@ -72,7 +111,7 @@ Loops nest, and `{ card.title }` style dotted paths reach into each row.
72
111
 
73
112
  Add `where <predicate>` to filter a loop. Conditions join with `and` / `or`:
74
113
 
75
- ```wd
114
+ ```wd-fragment
76
115
  @loop /products.json into p where p.featured == true and p.price < 80
77
116
  - { p.name }
78
117
  @endloop
@@ -100,12 +139,22 @@ The predicate decides reactivity exactly like the source does: read only the row
100
139
  A `:button` inside a reactive loop can act on its own row — the basis for carts and to-do lists:
101
140
 
102
141
  ```wd
142
+ :state products = [{"id": 1, "name": "Aurora", "price": 49}]
143
+ :state cart = []
144
+
103
145
  @loop products into product
146
+ ::: card
147
+ **{ product.name }** — ${ product.price }
104
148
  :button "Add to cart" -> cart += product
105
149
  :::
150
+ @endloop
151
+
106
152
  @loop cart into line
153
+ ::: card
154
+ { line.name }
107
155
  :button "Remove" -> cart remove line
108
156
  :::
157
+ @endloop
109
158
  ```
110
159
 
111
160
  `cart += product` appends a copy of the current row to another `:state` list; `cart remove line` removes the current row from the list being looped. Both are checked against the enclosing loop at compile time — no JavaScript.
@@ -114,7 +163,7 @@ A `:button` inside a reactive loop can act on its own row — the basis for cart
114
163
 
115
164
  Shape a loop with clauses in a fixed order after `into`: `where`, then `sort by`, `reverse`, `offset`, `limit`.
116
165
 
117
- ```wd
166
+ ```wd-fragment
118
167
  @loop /posts.json into post sort by post.date desc limit 5
119
168
  { $number }. { post.title }
120
169
  @endloop
@@ -125,7 +174,7 @@ Shape a loop with clauses in a fixed order after `into`: `where`, then `sort by`
125
174
 
126
175
  Every row exposes meta variables — `{ $index }` (0-based), `{ $number }` (1-based), `{ $first }`, `{ $last }`, `{ $count }` — usable in interpolation and `:if`:
127
176
 
128
- ```wd
177
+ ```wd-fragment
129
178
  @loop products into product
130
179
  :if $first
131
180
  **Top pick:**
@@ -138,7 +187,7 @@ Every row exposes meta variables — `{ $index }` (0-based), `{ $number }` (1-ba
138
187
 
139
188
  Add an `@empty` branch to render a fallback when the loop produces no rows (after filtering and slicing):
140
189
 
141
- ```wd
190
+ ```wd-fragment
142
191
  @loop todos into todo
143
192
  - { todo.title }
144
193
  @empty
@@ -146,8 +195,57 @@ Nothing left to do.
146
195
  @endloop
147
196
  ```
148
197
 
198
+ A **missing** in-scope source is an empty list, not an error — `@loop meta.tags into tag` on a page whose frontmatter omits `tags` (an optional field) loops zero rows and renders the `@empty` branch. A value that is present but not a list is still a compile error with the file and line.
199
+
149
200
  These clauses stay build-time when the source and every argument are static — a sorted, limited loop over a JSON file ships zero JS. The loop turns reactive only when the source or a clause reads `:state`/`:store`/`:fetch` data.
150
201
 
202
+ ## Content collections
203
+
204
+ Any folder under `site/pages/` is a **collection** — loop it by its bare name. No `content/` root, no marker file: a `site/pages/blog/` folder of posts *is* the `blog` collection. The whole [blog on this site](/blog/) is one such loop.
205
+
206
+ ```wd-fragment
207
+ @loop blog into post sort by post.date desc
208
+ - [{ post.title }]({ post.url }) — { post.date }
209
+ @endloop
210
+ ```
211
+
212
+ Each entry's frontmatter is a row, plus three derived fields: `{ post.url }` (the route), `{ post.slug }` (the filename), and `{ post.excerpt }` (the frontmatter `excerpt:`, else the first paragraph of a `.md` body). It's the same loop with the same clauses, resolved at build time — a pure listing ships zero JS. Drafts never appear in a default-build listing (only under `darkmown build --drafts`).
213
+
214
+ ### Typed schema — `_schema.wd`
215
+
216
+ Drop a `_schema.wd` at a collection's root to validate every entry at build time. It's frontmatter-shaped, one `field: type` per line:
217
+
218
+ ```wd
219
+ ---
220
+ title: string
221
+ date: date
222
+ excerpt: string?
223
+ tags: string[]?
224
+ ---
225
+ ```
226
+
227
+ Closed types: `string`, `number`, `boolean`, `date`, `string[]`, each with `?` for optional. A missing required field, a wrong type, an unknown extra field (typo guard), or an unknown type token fails the build with a `file:line`. No `_schema.wd` means no validation — it's opt-in.
228
+
229
+ ### Pagination — `paginate N`
230
+
231
+ Add `paginate N` (collections only) to split a listing into static pages. Page 1 keeps the listing route; pages 2+ are at `/<route>/page/2/`, `/page/3/`, … all static HTML.
232
+
233
+ ```wd-fragment
234
+ @loop blog into post sort by post.date desc paginate 5
235
+ - [{ post.title }]({ post.url })
236
+ @endloop
237
+
238
+ Page { page.current } of { page.total }
239
+ :if page.prev
240
+ [← Newer]({ page.prev })
241
+ :endif
242
+ :if page.next
243
+ [Older →]({ page.next })
244
+ :endif
245
+ ```
246
+
247
+ The pager — `{ page.current }`, `{ page.total }`, `{ page.prev }`, `{ page.next }` (URLs, empty at the ends) — is exposed to the whole page as plain links. Zero JS. `paginate` can't combine with `offset`/`limit` (it owns the slice).
248
+
151
249
  ## Interpolation
152
250
 
153
251
  One syntax everywhere: `{ name }` or `{ name.path }`.
@@ -161,6 +259,8 @@ One syntax everywhere: `{ name }` or `{ name.path }`.
161
259
 
162
260
  Shape a value for display with a pipe: `{ price | money }`, `{ joinedAt | date:"medium" }`, `{ ratio | percent }`. Pipes chain (`{ name | trim | capitalize }`) and take literal arguments (`{ total | money:"EUR" }`, `{ bio | truncate:80 }`). The same syntax works on static values (folded at build time, zero-JS) and live bindings (re-applied on render).
163
261
 
262
+ The value being piped must be **in scope** — a loop variable, `:state`/`:store`, an include argument, or `meta`. A bare literal like `{ 89 | money }` is not a name in scope, so it stays literal text (braces and all), exactly like any unresolved `{ … }`. A date-only value (`"2026-06-22"`) formats in UTC, so `date` prints the written calendar date on any build machine.
263
+
164
264
  The whitelist: `money`, `number`, `percent`, `round`, `date` / `time` / `datetime` (Intl-backed; `short` / `medium` / `long`), `upper` / `lower` / `capitalize`, `truncate:n` / `trim`, `pluralize:"item"`, and `default:"—"`. The five aggregates double as list pipes — `{ cart | count }`, `{ cart | sum:"price" | money }`, plus `avg` / `min` / `max`. No custom functions, nothing `eval`'d; an unknown pipe is a compile error. There is deliberately no relative "time ago" formatter, so builds stay reproducible.
165
265
 
166
266
  ## Frontmatter
@@ -176,6 +276,8 @@ tags: [sales, revenue, "q1, q2"]
176
276
 
177
277
  `{ meta.title }` prints a scalar and `{ meta.tags }` joins an array with `, `. You can loop a frontmatter array at build time — `@loop meta.tags into tag` — and the page stays static. Arrays are inline flow only (`[a, b]`); quoted items keep their commas.
178
278
 
279
+ `lang:` sets the document language on `<html lang>` per page (`lang: fr`, `lang: pt-BR`, …); it defaults to `en`. Every page also ships a visually-hidden-until-focused **skip-to-content link** targeting the main landmark — your own `<main>` is reused (an existing `id` wins as the target; an id-less one gets `id="main"`), and a page without one is wrapped in `<main id="main">`. Restyle it via `.wd-skip-link` in a skin. All compile-time — static pages stay zero-JS.
280
+
179
281
  ## SEO & feeds
180
282
 
181
283
  `darkmown build` writes `sitemap.xml`, `rss.xml`, and `robots.txt` — all build-time, zero client JS. There's no config file: the **home page** frontmatter carries the site's identity. Add `site_url` (absolute origin, no trailing slash) and the sitemap and feed switch on, prefixed with that origin and titled with the home `title`/`description`.
@@ -264,7 +366,7 @@ The aggregate's field is a bare row key (`price`, not `item.price`).
264
366
 
265
367
  A condition reads the same predicate grammar as `.class when` — a bare path (truthy), or the comparisons `==` `!=` `<` `<=` `>` `>=` `contains`, joined with `and`, `or`, and `not`. (`@loop … where` is the comparison-only subset.) Chain with `:else if` (any number; a bare `:else` must come last):
266
368
 
267
- ```wd
369
+ ```wd-fragment
268
370
  :if plan == "pro" or seats >= 5
269
371
  Pro plan
270
372
  :else if trialDays > 0 and not expired
@@ -280,7 +382,7 @@ A whole chain compiles to nested conditional regions, so it folds at build time
280
382
 
281
383
  A container class can react to state or the loop item. Static `.class` tokens stay as-is; add `when <predicate>` for a reactive one:
282
384
 
283
- ```wd
385
+ ```wd-fragment
284
386
  @loop products into p
285
387
  ::: card .product .on-sale when p.price < 50
286
388
  **{ p.name }** — ${ p.price }
@@ -315,7 +417,7 @@ A `:button "Label" -> action` mutates one `:state` or `:store` value. The vocabu
315
417
 
316
418
  Values are literals — a `"string"`, number, `true`/`false`/`null`, or inline JSON. Targets may be dotted paths (`cart.count++`), and one button can run several actions separated by `;` (applied in order, rendered once):
317
419
 
318
- ```wd
420
+ ```wd-fragment
319
421
  :button "Add to cart" -> cart.count++ ; cart.total += 9
320
422
  ```
321
423
 
@@ -341,6 +443,8 @@ No team members yet.
341
443
  :endif
342
444
  ```
343
445
 
446
+ The lifecycle regions announce themselves to assistive tech: a bare `:if name_loading` compiles with `role="status" aria-live="polite"` and `:if name_error` with `role="alert"` — compile-time attributes, no extra runtime JS. Write your own `role`/`aria-live` inside a region and nothing is added.
447
+
344
448
  Options after the URL: `method=` (default `GET`), `when=load|visible`, `timeout=<ms>`, `retry=<N>`, `headers=<key>`, and `body=<key>` (the last two name a `:state`/`:store` key). A URL can interpolate state (`"/api/users/{ userId }"`) and re-fetches automatically when that state changes; trigger a reload by hand with `:button "Reload" -> team refetch`. Loop a sub-path of fetched data with a dotted source — `@loop team.members into member`. URLs must be relative, `http(s)://`, or a leading `{ state }` interpolation — other schemes are rejected at compile time.
345
449
 
346
450
  ### Authenticated requests
@@ -483,10 +587,10 @@ The framework ships a small default `$code-*` set, so highlighting looks right o
483
587
  Darkmown is a trusted-author site generator: you compile content you wrote, the way you trust your own source. Three things define that boundary.
484
588
 
485
589
  - **Compile only content you authored.** Don't run `.md`/`.wd` you didn't write — user-generated content, third-party docs — through the compiler without sanitizing it first.
486
- - **Raw HTML passes through by default.** Markdown renders with `html: true`, so raw HTML is emitted verbatim and there is no built-in sanitizer. Set `html: false` in a page's frontmatter to drop raw HTML for untrusted content.
590
+ - **Raw HTML is escaped by default.** Markdown renders with `html: false`, so a stray `<script>` in content becomes inert text — safe for multi-author collections out of the box. Set `html: true` in a page's frontmatter to pass your own raw HTML through verbatim; there is no built-in sanitizer, so keep it off pages with content you didn't write.
487
591
  - **`:fetch` and `:form action=` have no host allowlist, and reactive pages need `unsafe-eval`.** Fetch/form URLs come straight from the page source — the compiler rejects non-http(s) schemes but does not restrict which hosts you call, so SSRF/exfiltration protection is the author's responsibility. The runtime compiles `:computed` / `@loop … where` / `.class when` expressions via `new Function`, so a reactive page needs `script-src 'unsafe-eval'` in your CSP. Static pages need neither.
488
592
 
489
- See [SECURITY.md](https://github.com/ZVN-DEV/darkmown/blob/main/SECURITY.md) in the package for the full security model.
593
+ See [SECURITY.md](https://github.com/ZVN-DEV/darkmown/blob/master/SECURITY.md) in the package for the full security model.
490
594
 
491
595
  ## Colocation
492
596
 
@@ -497,7 +601,7 @@ See [SECURITY.md](https://github.com/ZVN-DEV/darkmown/blob/main/SECURITY.md) in
497
601
 
498
602
  ## Spec status
499
603
 
500
- The implementation is faithful to the original core thesis: Markdown-first authoring, no component ceremony, zero runtime on static pages, and tiny direct-DOM reactivity only when declared. `:store`, the full `:fetch` lifecycle (loading/error/empty states, dynamic URLs, `refetch`, dotted loop sources, authenticated `headers=` + `refresh=` token renewal), `:form` (including server round-trips), loop ergonomics (`sort by`/`limit`/`offset`/`reverse`/`@empty` and `$index`/`$number`/`$first`/`$last`/`$count`), `:if … :else if … :else`, reactive `.class when …`, `:effect`, `:computed`, and `persist` are all shipped and live — try them on the [Data & Forms page](/data/) and the [Reactive page](/reactive/). Still on the roadmap: a first-party server runtime (`site/api/`) and HTML-fragment swaps. See `docs/spec-alignment.md` in the package for the full audit.
604
+ The implementation is faithful to the original core thesis: Markdown-first authoring, no component ceremony, zero runtime on static pages, and tiny direct-DOM reactivity only when declared. `:store`, the full `:fetch` lifecycle (loading/error/empty states, dynamic URLs, `refetch`, dotted loop sources, authenticated `headers=` + `refresh=` token renewal), `:form` (including server round-trips), loop ergonomics (`sort by`/`limit`/`offset`/`reverse`/`@empty` and `$index`/`$number`/`$first`/`$last`/`$count`), `:if … :else if … :else`, reactive `.class when …`, `:effect`, `:computed`, and `persist` are all shipped and live — try them on the [Data & Forms page](/data/) and the [Reactive page](/reactive/). Still on the roadmap: HTML-fragment `swap` semantics and cart server sync. (The serverless `api/` model already shipped in 1.2.0 — see [Backends & deploy](#backends--deploy) above; a `.wd` backend DSL remains a deliberate non-goal.) See `docs/spec-alignment.md` in the package for the full audit.
501
605
 
502
606
  See these directives composed into real apps on the [showcase](/showcase/).
503
607
 
@@ -1,3 +1,7 @@
1
+ ---
2
+ html: true
3
+ ---
4
+
1
5
  <!-- Folio — shared style carrier.
2
6
 
3
7
  Both the catalog (/folio/) and the cart (/folio/cart/) `@include ./style.wd`
@@ -1,4 +1,5 @@
1
1
  ---
2
+ html: true
2
3
  title: Folio — your basket
3
4
  description: The Folio checkout. The same :store cart shared with the catalog, a per-row remove, and a form that round-trips to a server — all written in Markdown.
4
5
  image: https://darkmown.com/__wd/media/og-folio.png
@@ -1,4 +1,5 @@
1
1
  ---
2
+ html: true
2
3
  title: Folio — a boutique storefront
3
4
  description: A multi-page store written in pure Markdown. The cart persists across pages, reloads, and browser tabs; navigation is instant. No framework JavaScript written by hand.
4
5
  image: https://darkmown.com/__wd/media/og-folio.png
@@ -1,4 +1,5 @@
1
1
  ---
2
+ html: true
2
3
  title: Forge your plan — a live pricing configurator in Markdown
3
4
  description: Build your plan, toggle features, adjust the seat count, and watch the price, savings, and upsell hints update instantly. A classic "this needs React" interaction — written as one Markdown file with a reactive :computed pricing engine.
4
5
  image: https://darkmown.com/__wd/media/og-forge.png
@@ -1,4 +1,5 @@
1
1
  ---
2
+ html: true
2
3
  title: Syntax highlighting — code that dark-modes for free
3
4
  description: Build-time syntax highlighting in plain Markdown. Fenced code blocks highlight at compile time (zero client JS) and recolor with your theme through the same tokens dark / :theme system — flip the toggle and watch every language recolor at once.
4
5
  image: https://darkmown.com/__wd/media/og-showcase.png
@@ -1,4 +1,5 @@
1
1
  ---
2
+ html: true
2
3
  title: Darkmown — Markdown that runs, not Markdown that needs React
3
4
  description: A live store widget — search filter, add-to-cart, running total — written as one Markdown file that runs. The same outcome in Markdoc needs a React component, hydration, and a build step. Compare them side by side.
4
5
  image: https://darkmown.com/__wd/media/og.png
@@ -1,4 +1,5 @@
1
1
  ---
2
+ html: true
2
3
  title: Interactions — sliders, drag & carousels in Markdown
3
4
  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
5
  image: https://darkmown.com/__wd/media/og-showcase.png
@@ -1,4 +1,5 @@
1
1
  ---
2
+ html: true
2
3
  title: Ledger — a sortable expense table in Markdown
3
4
  description: A real data table written as one Markdown file — clickable-header sorting, a live filter, running totals, and a light/dark toggle. Format pipes (| money, | date), aggregates (sum, avg, max), reactive sort, and :theme — all declarative, no hand-written JavaScript.
4
5
  image: https://darkmown.com/__wd/media/og-showcase.png
@@ -32,28 +32,65 @@ a
32
32
  color $accent
33
33
 
34
34
  button
35
- border 1px solid $ink
36
- bg $ink
35
+ display inline-flex
36
+ align-items center
37
+ justify-content center
38
+ gap .5rem
39
+ border 1px solid $accent
40
+ bg $accent
37
41
  color white
38
- padding .72rem 1rem
42
+ padding .6rem 1.1rem
39
43
  margin .3rem .5rem .3rem 0
40
- radius $radius
41
- font inherit
44
+ radius 999px
45
+ font system-ui, -apple-system, "Segoe UI", Roboto, sans-serif
46
+ font-size .92rem
47
+ font-weight 560
48
+ line-height 1.15
42
49
  cursor pointer
50
+ appearance none
51
+ box-shadow 0 1px 2px rgba(23,23,23,.08)
52
+ transition transform .14s cubic-bezier(.2,.7,.3,1), box-shadow .14s ease, background .14s ease, border-color .14s ease
43
53
 
44
54
  button:hover
45
55
  bg $accent
46
56
  border-color $accent
57
+ transform translateY(-1px)
58
+ box-shadow 0 8px 18px -8px rgba(22,100,90,.5)
59
+
60
+ button:active
61
+ transform translateY(0)
62
+ box-shadow 0 1px 2px rgba(23,23,23,.08)
63
+
64
+ button:focus-visible
65
+ outline 2px solid $accent
66
+ outline-offset 2px
67
+
68
+ button:disabled
69
+ opacity .5
70
+ cursor not-allowed
71
+ box-shadow none
72
+ transform none
47
73
 
48
74
  input
49
- padding .7rem .9rem
75
+ padding .62rem .85rem
50
76
  border 1px solid $line
51
77
  radius $radius
52
78
  font inherit
79
+ font-size .95rem
53
80
  bg $panel
54
81
  width 100%
55
82
  max-width 360px
56
83
  margin 1rem 0
84
+ appearance none
85
+ transition border-color .14s ease, box-shadow .14s ease
86
+
87
+ input:hover
88
+ border-color $accent
89
+
90
+ input:focus-visible
91
+ outline none
92
+ border-color $accent
93
+ box-shadow 0 0 0 3px rgba(22,100,90,.18)
57
94
 
58
95
  ul
59
96
  border 1px solid $line
@@ -1,4 +1,5 @@
1
1
  ---
2
+ html: true
2
3
  title: Loop ergonomics
3
4
  transitions: true
4
5
  ---
@@ -1,4 +1,5 @@
1
1
  ---
2
+ html: true
2
3
  title: Media — video, audio, and embeds in Markdown
3
4
  description: Three one-line directives — :video, :audio, :embed — compile to hardened, accessible, privacy-friendly players. No framework JavaScript ships: a media page stays zero-JS, just like static Markdown.
4
5
  image: https://darkmown.com/__wd/media/og-showcase.png
@@ -1,4 +1,5 @@
1
1
  ---
2
+ html: true
2
3
  title: Pulse — service status
3
4
  description: A calm, dark observability panel built in one Markdown file — a live :fetch board, build-time rollups, and a keyed services table, all with zero hand-written JavaScript.
4
5
  image: https://darkmown.com/__wd/media/og-pulse.png
@@ -29,18 +29,44 @@ h2
29
29
  padding-top 1.2rem
30
30
 
31
31
  button
32
- border 1px solid $ink
33
- bg $ink
32
+ display inline-flex
33
+ align-items center
34
+ justify-content center
35
+ gap .5rem
36
+ border 1px solid $accent
37
+ bg $accent
34
38
  color white
35
- padding .72rem 1rem
39
+ padding .6rem 1.1rem
36
40
  margin .3rem .5rem .3rem 0
37
- radius $radius
38
- font inherit
41
+ radius 999px
42
+ font system-ui, -apple-system, "Segoe UI", Roboto, sans-serif
43
+ font-size .92rem
44
+ font-weight 560
45
+ line-height 1.15
39
46
  cursor pointer
47
+ appearance none
48
+ box-shadow 0 1px 2px rgba(23,23,23,.08)
49
+ transition transform .14s cubic-bezier(.2,.7,.3,1), box-shadow .14s ease, background .14s ease, border-color .14s ease
40
50
 
41
51
  button:hover
42
52
  bg $accent
43
53
  border-color $accent
54
+ transform translateY(-1px)
55
+ box-shadow 0 8px 18px -8px rgba(22,100,90,.5)
56
+
57
+ button:active
58
+ transform translateY(0)
59
+ box-shadow 0 1px 2px rgba(23,23,23,.08)
60
+
61
+ button:focus-visible
62
+ outline 2px solid $accent
63
+ outline-offset 2px
64
+
65
+ button:disabled
66
+ opacity .5
67
+ cursor not-allowed
68
+ box-shadow none
69
+ transform none
44
70
 
45
71
  a
46
72
  color $accent
@@ -89,4 +115,10 @@ ul
89
115
  bg transparent
90
116
  color $warm
91
117
  border 1px solid $line
92
- padding .35rem .8rem
118
+ padding .4rem .9rem
119
+ box-shadow none
120
+
121
+ .card.line button:hover
122
+ bg $warm
123
+ border-color $warm
124
+ color white
@@ -1,4 +1,5 @@
1
1
  ---
2
+ html: true
2
3
  title: Reactive Markdown
3
4
  transitions: true
4
5
  ---
@@ -1,4 +1,5 @@
1
1
  ---
2
+ html: true
2
3
  title: Scoped styles — same class name, zero collision
3
4
  description: Two components reuse the class name .card and never clash. Compile-time scoped styles stamp each component's subtree with a path-derived id and rewrite its skin's selectors to match only there. Zero runtime JS — a static page stays static.
4
5
  transitions: true
@@ -1,4 +1,5 @@
1
1
  ---
2
+ html: true
2
3
  title: Showcase — real apps built in Markdown
3
4
  description: Five complete, beautiful apps — a storefront, a live dashboard, a pricing configurator, a product-finder quiz, and a sortable expense ledger — each one a readable Markdown file. See the app, then view its source.
4
5
  image: https://darkmown.com/__wd/media/og-showcase.png
@@ -32,28 +32,65 @@ a
32
32
  color $accent
33
33
 
34
34
  button
35
- border 1px solid $ink
36
- bg $ink
35
+ display inline-flex
36
+ align-items center
37
+ justify-content center
38
+ gap .5rem
39
+ border 1px solid $accent
40
+ bg $accent
37
41
  color white
38
- padding .72rem 1rem
42
+ padding .6rem 1.1rem
39
43
  margin .3rem .5rem .3rem 0
40
- radius $radius
41
- font inherit
44
+ radius 999px
45
+ font system-ui, -apple-system, "Segoe UI", Roboto, sans-serif
46
+ font-size .92rem
47
+ font-weight 560
48
+ line-height 1.15
42
49
  cursor pointer
50
+ appearance none
51
+ box-shadow 0 1px 2px rgba(23,23,23,.08)
52
+ transition transform .14s cubic-bezier(.2,.7,.3,1), box-shadow .14s ease, background .14s ease, border-color .14s ease
43
53
 
44
54
  button:hover
45
55
  bg $accent
46
56
  border-color $accent
57
+ transform translateY(-1px)
58
+ box-shadow 0 8px 18px -8px rgba(22,100,90,.5)
59
+
60
+ button:active
61
+ transform translateY(0)
62
+ box-shadow 0 1px 2px rgba(23,23,23,.08)
63
+
64
+ button:focus-visible
65
+ outline 2px solid $accent
66
+ outline-offset 2px
67
+
68
+ button:disabled
69
+ opacity .5
70
+ cursor not-allowed
71
+ box-shadow none
72
+ transform none
47
73
 
48
74
  input
49
- padding .7rem .9rem
75
+ padding .62rem .85rem
50
76
  border 1px solid $line
51
77
  radius $radius
52
78
  font inherit
79
+ font-size .95rem
53
80
  bg $panel
54
81
  width 100%
55
82
  max-width 360px
56
83
  margin 1rem 0
84
+ appearance none
85
+ transition border-color .14s ease, box-shadow .14s ease
86
+
87
+ input:hover
88
+ border-color $accent
89
+
90
+ input:focus-visible
91
+ outline none
92
+ border-color $accent
93
+ box-shadow 0 0 0 3px rgba(22,100,90,.18)
57
94
 
58
95
  #shop
59
96
  border 1px solid $line
@@ -1,4 +1,5 @@
1
1
  ---
2
+ html: true
2
3
  title: Global store
3
4
  transitions: true
4
5
  ---
@@ -1,4 +1,5 @@
1
1
  ---
2
+ html: true
2
3
  title: Dark-mode tokens
3
4
  transitions: true
4
5
  ---