@zvndev/darkmown 0.19.0 → 1.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 (69) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +25 -0
  3. package/README.md +115 -5
  4. package/docs/spec-alignment.md +16 -3
  5. package/package.json +22 -4
  6. package/site/_/base.skin +372 -0
  7. package/site/_/base.wd +4 -0
  8. package/site/_/footer.wd +14 -0
  9. package/site/_/metrics.json +13 -0
  10. package/site/_/nav.skin +35 -12
  11. package/site/_/nav.wd +8 -10
  12. package/site/_/products.json +10 -0
  13. package/site/pages/app.wd +1 -1
  14. package/site/pages/carousel/index.js +82 -0
  15. package/site/pages/carousel/index.skin +185 -0
  16. package/site/pages/carousel/index.wd +109 -0
  17. package/site/pages/carousel/slide-1.jpg +0 -0
  18. package/site/pages/carousel/slide-2.jpg +0 -0
  19. package/site/pages/carousel/slide-3.jpg +0 -0
  20. package/site/pages/carousel/slide-4.jpg +0 -0
  21. package/site/pages/carousel/slide-5.jpg +0 -0
  22. package/site/pages/compass/index.skin +247 -0
  23. package/site/pages/compass/index.wd +194 -0
  24. package/site/pages/docs/index.skin +48 -28
  25. package/site/pages/docs/index.wd +71 -5
  26. package/site/pages/folio/-style.skin +375 -0
  27. package/site/pages/folio/-style.wd +6 -0
  28. package/site/pages/folio/cart.skin +309 -0
  29. package/site/pages/folio/cart.wd +201 -0
  30. package/site/pages/folio/index.skin +24 -0
  31. package/site/pages/folio/index.wd +167 -0
  32. package/site/pages/forge/index.skin +676 -0
  33. package/site/pages/forge/index.wd +330 -0
  34. package/site/pages/index.skin +135 -144
  35. package/site/pages/index.wd +17 -5
  36. package/site/pages/ledger/index.skin +257 -0
  37. package/site/pages/ledger/index.wd +174 -0
  38. package/site/pages/media/clip-poster.jpg +0 -0
  39. package/site/pages/media/clip.mp4 +0 -0
  40. package/site/pages/media/index.skin +82 -0
  41. package/site/pages/media/index.wd +84 -0
  42. package/site/pages/media/tone.mp3 +0 -0
  43. package/site/pages/pulse/index.skin +434 -0
  44. package/site/pages/pulse/index.wd +178 -0
  45. package/site/pages/showcase/index.skin +130 -0
  46. package/site/pages/showcase/index.wd +168 -0
  47. package/src/builder.js +20 -14
  48. package/src/cli.js +216 -77
  49. package/src/compiler/body.js +457 -0
  50. package/src/compiler/context.js +160 -0
  51. package/src/compiler/directives.js +1345 -0
  52. package/src/compiler/format.js +244 -0
  53. package/src/compiler/frontmatter.js +117 -0
  54. package/src/compiler/includes.js +116 -0
  55. package/src/compiler/index.js +22 -0
  56. package/src/compiler/interpolation.js +223 -0
  57. package/src/compiler/loops.js +715 -0
  58. package/src/compiler/markdown.js +242 -0
  59. package/src/compiler/page.js +267 -0
  60. package/src/compiler/predicates.js +314 -0
  61. package/src/compiler.js +26 -2932
  62. package/src/dev.js +3 -1
  63. package/src/headers.js +8 -4
  64. package/src/index.js +22 -0
  65. package/src/router.js +3 -1
  66. package/src/runtime.js +168 -14
  67. package/src/scaffold.js +158 -131
  68. package/src/skin.js +11 -2
  69. package/site/_/og.png +0 -0
@@ -0,0 +1,194 @@
1
+ ---
2
+ title: Compass — find your match
3
+ description: A guided product-finder quiz built in one Markdown file. Four questions advance a state machine with :if branching, accumulate scores with :button actions, and a :computed recommendation lands the perfect pick. Zero hand-written JavaScript.
4
+ image: https://darkmown.com/__wd/media/og-compass.png
5
+ transitions: true
6
+ ---
7
+
8
+ @include /nav.wd
9
+ @include /base.wd
10
+
11
+ <style>@keyframes pop { 0% { transform: scale(0) rotate(-18deg); opacity: 0 } 100% { transform: scale(1) rotate(0); opacity: 1 } }</style>
12
+
13
+ <main class="app-compass">
14
+
15
+ :state step = 1
16
+ :state calm = 0
17
+ :state bold = 0
18
+ :state budget = 0
19
+
20
+ ::: section #quiz .compass-stage
21
+
22
+ ::: compass-card .compass-card
23
+
24
+ :if step <= 4
25
+
26
+ ::: compass-top .compass-top
27
+
28
+ ::: eyebrow .compass-eyebrow
29
+ Question { step } of 4
30
+ :::
31
+
32
+ ::: compass-rail .compass-rail .at-1 when step == 1 .at-2 when step == 2 .at-3 when step == 3 .at-4 when step == 4
33
+ :::
34
+
35
+ :::
36
+
37
+ :endif
38
+
39
+ :if step == 1
40
+
41
+ ## Where should it live?
42
+
43
+ ::: lead .compass-lead
44
+ Picture the spot you'd reach for it most. That tells us a lot.
45
+ :::
46
+
47
+ ::: answers .compass-answers
48
+ :button "🛋️ A cozy nook to unwind" -> calm += 2; step++
49
+ :button "🎉 The room where people gather" -> bold += 2; step++
50
+ :button "🧺 Wherever — it should just be useful" -> budget += 2; step++
51
+ :::
52
+
53
+ :else if step == 2
54
+
55
+ ## What's the mood you're after?
56
+
57
+ ::: lead .compass-lead
58
+ No wrong answers. Go with your gut.
59
+ :::
60
+
61
+ ::: answers .compass-answers
62
+ :button "🌙 Soft, calm, a little dreamy" -> calm += 2; step++
63
+ :button "⚡ Lively, bold, turn it up" -> bold += 2; step++
64
+ :button "🧘 Fresh and easy — no fuss" -> calm += 1; budget += 1; step++
65
+ :::
66
+
67
+ :else if step == 3
68
+
69
+ ## How does it earn its keep?
70
+
71
+ ::: lead .compass-lead
72
+ The thing it has to nail before anything else.
73
+ :::
74
+
75
+ ::: answers .compass-answers
76
+ :button "✨ It sets the whole vibe" -> calm += 2; step++
77
+ :button "🔊 It fills the room" -> bold += 2; step++
78
+ :button "💧 It quietly makes the day nicer" -> calm += 1; budget += 1; step++
79
+ :::
80
+
81
+ :else if step == 4
82
+
83
+ ## And the budget?
84
+
85
+ ::: lead .compass-lead
86
+ Last one — then we'll find your match.
87
+ :::
88
+
89
+ ::: answers .compass-answers
90
+ :button "🌱 Keep it gentle" -> budget += 3; step++
91
+ :button "⚖️ A fair middle" -> budget += 1; step++
92
+ :button "💎 Splurge for the right one" -> bold += 2; step++
93
+ :::
94
+
95
+ :else
96
+
97
+ ::: result .compass-result
98
+
99
+ ::: burst .compass-burst
100
+ 🎉
101
+ :::
102
+
103
+ ::: eyebrow .compass-eyebrow .is-result
104
+ Your match
105
+ :::
106
+
107
+ :if bold > calm and bold > budget
108
+
109
+ # The Cove Speaker
110
+
111
+ ::: why .compass-why
112
+ You're here to fill the room. The **Cove Speaker** throws warm, room-filling 360° sound out of a palm-sized shell — the heart of every gathering, turned all the way up.
113
+ :::
114
+
115
+ [Shop this →](/folio/){.compass-cta}
116
+
117
+ :else if calm > bold and calm > budget
118
+
119
+ # The Aurora Lamp
120
+
121
+ ::: why .compass-why
122
+ You want a space that *feels* like something. The **Aurora Lamp** pours a warm, dimmable glow from a hand-turned oak base — the quiet centerpiece of your cozy nook.
123
+ :::
124
+
125
+ [Shop this →](/folio/){.compass-cta}
126
+
127
+ :else if budget > calm and budget > bold
128
+
129
+ # The Haven Diffuser
130
+
131
+ ::: why .compass-why
132
+ You like things that quietly make the day nicer — without the splurge. The **Haven Diffuser** breathes a soft cedar-scented mist into the room, refillable and easy to love.
133
+ :::
134
+
135
+ [Shop this →](/folio/){.compass-cta}
136
+
137
+ :else
138
+
139
+ # The Isle Earbuds
140
+
141
+ ::: why .compass-why
142
+ You're a little of everything — calm, bold, and practical. The **Isle Earbuds** go anywhere you do: active noise cancelling, a 30-hour case, and tap controls for whatever the day brings.
143
+ :::
144
+
145
+ [Shop this →](/folio/){.compass-cta}
146
+
147
+ :endif
148
+
149
+ ::: restart .compass-restart
150
+ :button "↺ Start over" -> step reset; calm reset; bold reset; budget reset
151
+ :::
152
+
153
+ :::
154
+
155
+ :endif
156
+
157
+ :::
158
+
159
+ :::
160
+
161
+ <details class="source-reveal">
162
+ <summary>View source — it's Markdown</summary>
163
+
164
+ ```wd
165
+ :state step = 1
166
+ :state calm = 0
167
+ :state bold = 0
168
+ :state budget = 0
169
+
170
+ :if step <= 4
171
+ Question { step } of 4
172
+ :endif
173
+
174
+ :if step == 1
175
+ ## Where should it live?
176
+ :button "🛋️ A cozy nook to unwind" -> calm += 2; step++
177
+ :button "🎉 The room where people gather" -> bold += 2; step++
178
+ :else if step == 2
179
+ ## What's the mood you're after?
180
+ :button "🌙 Soft, calm, a little dreamy" -> calm += 2; step++
181
+ ... three more questions ...
182
+ :else
183
+ :if bold > calm and bold > budget
184
+ # The Cove Speaker
185
+ :else if calm > bold and calm > budget
186
+ # The Aurora Lamp
187
+ :endif
188
+ :button "↺ Start over" -> step reset; calm reset; bold reset; budget reset
189
+ :endif
190
+ ```
191
+
192
+ </details>
193
+
194
+ </main>
@@ -1,39 +1,59 @@
1
- tokens
2
- ink #171717
3
- paper #fbfaf7
4
- panel #ffffff
5
- line #dedbd3
6
- accent #16645a
7
- muted #666158
8
- radius 8px
9
-
10
- page
11
- margin 0
12
- font ui-serif, Georgia, Cambria, serif
13
- color $ink
14
- bg $paper
15
- line-height 1.7
1
+ /* Docs — layers on /base.wd (tokens, type, code, dark mode). Refined,
2
+ readable single-column reference. Only docs-specific layout here. */
16
3
 
17
- main
18
- max-width 820px
19
- margin 0 auto
20
- padding 3rem 2rem 5rem
4
+ main.docs
5
+ max-width 760px
21
6
 
22
- h1
23
- font-size 3rem
24
- line-height 1
25
- margin 1rem 0
7
+ .docs-hero
8
+ padding 2rem 0 1.5rem
9
+ margin-bottom 1rem
10
+ border-bottom 1px solid $line
11
+
12
+ .docs-hero h1
13
+ margin .2rem 0 .5rem
14
+
15
+ .docs-hero p
16
+ font-size 1.12rem
17
+ color $ink-soft
18
+ max-width 60ch
26
19
 
27
- h2
28
- margin-top 2.4rem
29
- padding-top 1.2rem
20
+ /* Section headings get a quiet rule above them so the long page scans. */
21
+ main.docs h2
22
+ margin-top 3rem
23
+ padding-top 1.4rem
30
24
  border-top 1px solid $line
25
+ font-size 1.55rem
31
26
 
32
- a
27
+ main.docs h3
28
+ margin-top 2rem
29
+ color $accent-deep
30
+
31
+ main.docs p, main.docs li
32
+ color $ink-soft
33
+
34
+ main.docs strong
35
+ color $ink
36
+
37
+ main.docs a
33
38
  color $accent
39
+ font-weight 550
40
+
41
+ /* Tight, legible lists for the rule-heavy reference sections. */
42
+ main.docs ul
43
+ padding-left 1.2rem
44
+
45
+ main.docs li
46
+ margin .35rem 0
47
+
48
+ main.docs li code
49
+ white-space nowrap
34
50
 
35
51
  .feature-card
36
52
  border 1px solid $line
37
53
  bg $panel
38
- padding 1rem
54
+ padding 1.1rem 1.25rem
39
55
  radius $radius
56
+ margin 1rem 0
57
+
58
+ .feature-card strong
59
+ color $warm
@@ -5,12 +5,17 @@ transitions: true
5
5
  ---
6
6
 
7
7
  @include /nav.wd
8
+ @include /base.wd
8
9
 
9
- <main>
10
+ <main class="docs">
10
11
 
11
- # Docs
12
+ <header class="docs-hero">
13
+ <span class="eyebrow">Documentation</span>
14
+
15
+ # The Darkmown docs
12
16
 
13
17
  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
+ </header>
14
19
 
15
20
  @include ./-relative-note.wd
16
21
 
@@ -152,6 +157,12 @@ One syntax everywhere: `{ name }` or `{ name.path }`.
152
157
  - The page's frontmatter is in scope as `meta` — `{ meta.title }` prints a field.
153
158
  - Otherwise the text is left exactly as written — braces in prose never break a page or pull in the runtime.
154
159
 
160
+ ### Format pipes — `{ value | name:arg }`
161
+
162
+ 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
+
164
+ 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
+
155
166
  ## Frontmatter
156
167
 
157
168
  Frontmatter sits between `---` fences. Values are strings, plus inline arrays:
@@ -179,7 +190,7 @@ You don't size images by hand. Every `<img>` the compiler emits gets its intrins
179
190
 
180
191
  ## Reactive directives
181
192
 
182
- Reactive pages opt into `/__wd/runtime.js` (~5.7 KB gzipped, under a CI-enforced 6 KB budget). Static pages do not.
193
+ Reactive pages opt into `/__wd/runtime.js` (~7.4 KB gzipped, under a CI-enforced 8 KB budget). Static pages do not.
183
194
 
184
195
  ```wd
185
196
  :state count = 0
@@ -205,6 +216,32 @@ Count is still zero.
205
216
 
206
217
  Directive actions are intentionally narrow and compile-time checked. Arbitrary JavaScript belongs in a colocated `.js` file.
207
218
 
219
+ ### Computed values and aggregates
220
+
221
+ `:computed name = expr` derives state from state — arithmetic, comparisons, and the five list aggregates `sum` / `avg` / `min` / `max` / `count`. It recomputes when an input changes and reads like any binding, so it pairs with format pipes:
222
+
223
+ ```wd
224
+ :store cart = [{"price": 89}, {"price": 12}]
225
+ :computed subtotal = sum(cart, price)
226
+ :computed total = subtotal * 1.08
227
+
228
+ Total: { total | money }
229
+ ```
230
+
231
+ The aggregate's field is a bare row key (`price`, not `item.price`).
232
+
233
+ ### Timers — `:every`
234
+
235
+ `:every <duration> -> <actions>` runs a `:button`-style action on an interval (`ms` / `s` / `m`). Intervals pause while the tab is hidden and resume on return.
236
+
237
+ ```wd
238
+ :fetch board from "/status.json"
239
+ :every 10s -> board refetch
240
+
241
+ :state secs = 0
242
+ :every 1s -> secs++
243
+ ```
244
+
208
245
  ### Multi-branch conditionals
209
246
 
210
247
  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):
@@ -320,10 +357,35 @@ You have { cart } items.
320
357
  - `:input field placeholder="…" required` and `:submit "Label"` build the form body.
321
358
  - `:state cart = [] persist` survives reloads via localStorage on a single page; use `:store` for state shared across pages and tabs.
322
359
  - `:if item.path` works inside reactive loops for per-row branches, and conditionals nest — an inner `:if` resolves after the outer branch, staying reactive.
323
- - Reactive pages expose `window.wd` (`get`, `set`, `state`, `render`) so colocated `.js` can do anything directives can't. Section state is addressed as `sectionId:name`.
360
+ - Reactive pages expose `window.wd` (`get`, `set`, `subscribe`, `state`, `render`) so colocated `.js` can do anything directives can't. Section state is addressed as `sectionId:name`.
324
361
 
325
362
  See it all live on the [Data & Forms page](/data/).
326
363
 
364
+ ## Theme toggle — `:theme`
365
+
366
+ 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>`:
367
+
368
+ ```wd
369
+ :theme
370
+ :button "Auto" -> theme = "auto"
371
+ :button "Light" -> theme = "light"
372
+ :button "Dark" -> theme = "dark"
373
+ ```
374
+
375
+ A single `tokens dark` block in your `.skin` already powers both the OS `prefers-color-scheme` query and this toggle — no extra block needed. `"auto"` clears the attribute and follows the OS again. See it on the [Ledger demo](/ledger/).
376
+
377
+ ## Media — `:video`, `:audio`, `:embed`
378
+
379
+ Three compile-time-only directives (they emit no `data-wd-*`, so a media page stays zero-JS):
380
+
381
+ ```wd
382
+ :video /clip.mp4 poster=/clip.jpg controls
383
+ :audio /track.mp3 controls
384
+ :embed https://youtu.be/aqz-KE-bpKQ title="Big Buck Bunny"
385
+ ```
386
+
387
+ `:video`/`:audio` emit a hardened HTML5 player — `preload="metadata"` and `controls` by default, whitelisted flags (`autoplay` implies `muted`), and the `:fetch` URL scheme guard. `:embed` rewrites a YouTube/Vimeo link to its no-cookie/player form in a lazy, responsive `16/9` iframe. See the zero-JS [Media demo](/media/).
388
+
327
389
  ## Trust boundary
328
390
 
329
391
  Darkmown is a trusted-author site generator: you compile content you wrote, the way you trust your own source. Three things define that boundary.
@@ -337,7 +399,7 @@ See [SECURITY.md](https://github.com/ZVN-DEV/darkmown/blob/main/SECURITY.md) in
337
399
  ## Colocation
338
400
 
339
401
  - A matching `.skin` file attaches CSS to the page (indentation-based, compiles to real CSS).
340
- - A matching `.js` file attaches page behavior.
402
+ - A matching `.js` file attaches page behavior. It loads **after** the runtime, so `window.wd` is ready: `wd.get(key)`, `wd.set(key, value)`, and `wd.subscribe(key, cb)` (runs now and on every settled change) bridge declarative state to imperative widgets — the gestures the framework leaves out (keyboard, drag/touch, canvas). The [Swiper demo](/carousel/) is a draggable carousel built this way.
341
403
  - Both work for included fragments too, by basename.
342
404
  - Any other file under `site/pages/` (an image, font, PDF — anything that isn't a `.md`/`.wd` route or a basename-matched `.skin`/`.js`) copies to `dist/` at its own path with the right content-type. So `site/pages/logo.svg` is served at `/logo.svg`. Assets below hidden path segments (`.`, `-`, or `_`) and symlinked page assets are skipped, matching the routing privacy rule. Use the `site/_/` shelf for assets shared across pages; colocate the ones a single page owns.
343
405
 
@@ -345,4 +407,8 @@ See [SECURITY.md](https://github.com/ZVN-DEV/darkmown/blob/main/SECURITY.md) in
345
407
 
346
408
  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.
347
409
 
410
+ See these directives composed into real apps on the [showcase](/showcase/).
411
+
348
412
  </main>
413
+
414
+ @include /footer.wd