@zvndev/darkmown 0.20.0 → 1.1.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 (57) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +29 -0
  3. package/README.md +125 -5
  4. package/docs/spec-alignment.md +23 -3
  5. package/package.json +3 -3
  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 +72 -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 +183 -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/compiler/body.js +89 -3
  48. package/src/compiler/context.js +1 -1
  49. package/src/compiler/directives.js +199 -3
  50. package/src/compiler/format.js +244 -0
  51. package/src/compiler/interpolation.js +36 -1
  52. package/src/compiler/loops.js +95 -24
  53. package/src/compiler/markdown.js +16 -7
  54. package/src/compiler/predicates.js +35 -2
  55. package/src/headers.js +5 -0
  56. package/src/runtime.js +168 -14
  57. package/src/skin.js +9 -1
@@ -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):
@@ -312,6 +349,7 @@ You have { cart } items.
312
349
  - Saved to localStorage under `wd:store:<name>` and reloaded on the next visit; changes sync live to other open tabs.
313
350
  - The declared value is a **seed**, used only the first time — afterward the persisted value wins.
314
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]"`.
315
353
  - Add `ephemeral` for an in-memory store that resets on reload: `:store sidebarOpen = false ephemeral`.
316
354
 
317
355
  ## Forms and persistence
@@ -320,10 +358,35 @@ You have { cart } items.
320
358
  - `:input field placeholder="…" required` and `:submit "Label"` build the form body.
321
359
  - `:state cart = [] persist` survives reloads via localStorage on a single page; use `:store` for state shared across pages and tabs.
322
360
  - `: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`.
361
+ - 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
362
 
325
363
  See it all live on the [Data & Forms page](/data/).
326
364
 
365
+ ## Theme toggle — `:theme`
366
+
367
+ 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>`:
368
+
369
+ ```wd
370
+ :theme
371
+ :button "Auto" -> theme = "auto"
372
+ :button "Light" -> theme = "light"
373
+ :button "Dark" -> theme = "dark"
374
+ ```
375
+
376
+ 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/).
377
+
378
+ ## Media — `:video`, `:audio`, `:embed`
379
+
380
+ Three compile-time-only directives (they emit no `data-wd-*`, so a media page stays zero-JS):
381
+
382
+ ```wd
383
+ :video /clip.mp4 poster=/clip.jpg controls
384
+ :audio /track.mp3 controls
385
+ :embed https://youtu.be/aqz-KE-bpKQ title="Big Buck Bunny"
386
+ ```
387
+
388
+ `: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/).
389
+
327
390
  ## Trust boundary
328
391
 
329
392
  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 +400,7 @@ See [SECURITY.md](https://github.com/ZVN-DEV/darkmown/blob/main/SECURITY.md) in
337
400
  ## Colocation
338
401
 
339
402
  - A matching `.skin` file attaches CSS to the page (indentation-based, compiles to real CSS).
340
- - A matching `.js` file attaches page behavior.
403
+ - 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
404
  - Both work for included fragments too, by basename.
342
405
  - 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
406
 
@@ -345,4 +408,8 @@ See [SECURITY.md](https://github.com/ZVN-DEV/darkmown/blob/main/SECURITY.md) in
345
408
 
346
409
  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
410
 
411
+ See these directives composed into real apps on the [showcase](/showcase/).
412
+
348
413
  </main>
414
+
415
+ @include /footer.wd