@zvndev/darkmown 0.4.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.
@@ -0,0 +1,110 @@
1
+ ---
2
+ title: Data, forms, and persistence
3
+ ---
4
+
5
+ @include /nav.wd
6
+
7
+ <main>
8
+
9
+ # Data, forms, and persistence
10
+
11
+ This page demos the Tier 1.5 surface: declarative fetch, forms that write state, persistent state, and the JavaScript escape hatch.
12
+
13
+ ## Fetched data drives reactive loops
14
+
15
+ `:fetch team from "/__wd/data/team.json"` declares state and fills it from the network. Until it lands, the page shows the fallback branch:
16
+
17
+ :fetch team from "/__wd/data/team.json"
18
+
19
+ :if team
20
+ @loop team into member
21
+ ::: card .member-card
22
+ **{ member.name }** — { member.role }
23
+ :if member.lead
24
+ Team lead ★
25
+ :else
26
+ Crew
27
+ :endif
28
+ :::
29
+ @endloop
30
+ :else
31
+ Loading the team…
32
+ :endif
33
+
34
+ That loop also proves `:if` over loop items: the lead badge is decided per row.
35
+
36
+ ## Forms write state, no backend required
37
+
38
+ `:form into profile` captures the submit straight into state. With `action="/url"` instead, Darkmown emits a plain native form and stays out of the way entirely:
39
+
40
+ :form into profile
41
+ :input name placeholder="Your name" required
42
+ :input quest placeholder="Your quest"
43
+ :submit "Save profile"
44
+ :endform
45
+
46
+ :if profile
47
+ Hello **{ profile.name }** — good luck with { profile.quest }.
48
+ :else
49
+ Submit the form and this sentence reacts. Nothing leaves the page.
50
+ :endif
51
+
52
+ ## Server round-trips, adapter style
53
+
54
+ 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:
55
+
56
+ :form action="/__wd/echo" into reply
57
+ :input message placeholder="Say something to the server"
58
+ :submit "Send to server"
59
+ :endform
60
+
61
+ :if reply
62
+ The server answered with { reply.received.message } at { reply.at }.
63
+ :else
64
+ The reply will appear here.
65
+ :endif
66
+
67
+ :if reply_error
68
+ The request failed: { reply_error }
69
+ :endif
70
+
71
+ 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. Either way, Darkmown itself stays static — sessions ride on `:fetch` plus ordinary cookies against your real API.
72
+
73
+ ## Persistent state survives reload
74
+
75
+ `:state items = [] persist` keeps this section in localStorage. Add a few, reload the page, and the count holds:
76
+
77
+ ::: section #cart
78
+ :state items = [] persist
79
+ :computed total = items.length * 4
80
+
81
+ The cart holds { items.length } item(s) worth ${ total }.
82
+
83
+ :button "Add sticker" -> items += {"id": "sticker", "name": "Sticker"}
84
+ :button "Add poster" -> items += {"id": "poster", "name": "Poster"}
85
+ :button "Empty cart" -> items = []
86
+ :::
87
+
88
+ `:computed total = items.length * 4` derives state from state — the price updates with every cart change, persistence included.
89
+
90
+ ## The escape hatch
91
+
92
+ Colocated `data.js` uses the `window.wd` API (`wd.get`, `wd.set`) for anything directives can't say. Section state is addressed as `cart:items`:
93
+
94
+ <button class="pulse-button" data-wd-double>Double the cart via window.wd</button>
95
+
96
+ ## Lazy loading
97
+
98
+ The list below uses `when=visible` — the request only fires once this section scrolls into view. Watch the network tab:
99
+
100
+ :fetch quotes from "/__wd/data/quotes.json" when=visible
101
+
102
+ :if quotes
103
+ @loop quotes into quote
104
+ - { quote }
105
+ @endloop
106
+ :else
107
+ Scroll me into view and I will load.
108
+ :endif
109
+
110
+ </main>
@@ -0,0 +1,5 @@
1
+ <section class="feature-card">
2
+
3
+ **Relative include:** this hidden block lives beside the docs page as `-relative-note.wd`, so it can be included without becoming a route.
4
+
5
+ </section>
@@ -0,0 +1,3 @@
1
+ # Secret
2
+
3
+ This hidden nested page should never be emitted.
@@ -0,0 +1,39 @@
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
16
+
17
+ main
18
+ max-width 820px
19
+ margin 0 auto
20
+ padding 3rem 2rem 5rem
21
+
22
+ h1
23
+ font-size 3rem
24
+ line-height 1
25
+ margin 1rem 0
26
+
27
+ h2
28
+ margin-top 2.4rem
29
+ padding-top 1.2rem
30
+ border-top 1px solid $line
31
+
32
+ a
33
+ color $accent
34
+
35
+ .feature-card
36
+ border 1px solid $line
37
+ bg $panel
38
+ padding 1rem
39
+ radius $radius
@@ -0,0 +1,125 @@
1
+ ---
2
+ title: Darkmown docs
3
+ ---
4
+
5
+ @include /nav.wd
6
+
7
+ <main>
8
+
9
+ # Docs
10
+
11
+ 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.
12
+
13
+ @include ./-relative-note.wd
14
+
15
+ ## Install
16
+
17
+ ```sh
18
+ npx @zvndev/darkmown init my-site
19
+ cd my-site
20
+ npm install
21
+ npm run dev
22
+ ```
23
+
24
+ Or add Darkmown to an existing project with `npm install -D @zvndev/darkmown`. The package is scoped; the command it installs is plain `darkmown`.
25
+
26
+ ## The two formats
27
+
28
+ - `.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.
29
+ - `.wd` is Markdown plus directives. Renaming a file from `.md` to `.wd` is the upgrade path — nothing else changes.
30
+
31
+ ## Routing rules
32
+
33
+ - `site/pages/index.wd` becomes `/`.
34
+ - `site/pages/docs/index.wd` becomes `/docs/`.
35
+ - `.secret.wd`, `-draft.wd`, and hidden folders do not become pages.
36
+ - `site/_/` is the include shelf, never a route.
37
+
38
+ ## Includes
39
+
40
+ - `@include /nav.wd` resolves from `site/_`.
41
+ - `@include ./-relative-note.wd` resolves beside the current page.
42
+ - `@include /card.wd with title="Hello" count=3` passes values into the include.
43
+ - `with title={ feature.title }` passes a value already in scope — including whole objects.
44
+ - Includes inside a loop inherit the loop value automatically.
45
+
46
+ ## Loops
47
+
48
+ `@loop <things> into <thing>` is the only loop. The body is normal Markdown (or more directives) and repeats once per row:
49
+
50
+ ```wd
51
+ @loop /features.json into card
52
+ @include /feature-card.wd
53
+ @endloop
54
+ ```
55
+
56
+ The source decides how the loop behaves:
57
+
58
+ - A JSON file path (`/features.json`, `./data.json`) unrolls at build time — pure static HTML.
59
+ - An in-scope value (an include argument or an outer loop value) also unrolls at build time.
60
+ - A `:state` list compiles to a reactive region the runtime patches by key.
61
+
62
+ Loops nest, and `{ card.title }` style dotted paths reach into each row.
63
+
64
+ ## Interpolation
65
+
66
+ One syntax everywhere: `{ name }` or `{ name.path }`.
67
+
68
+ - If the name is a static value in scope (include argument, loop value), it is resolved at build time.
69
+ - If the name is declared `:state`, it becomes a live binding.
70
+ - Otherwise the text is left exactly as written — braces in prose never break a page or pull in the runtime.
71
+
72
+ ## Sections
73
+
74
+ `::: section #id .class` opens a container and `:::` closes it. Sections scope state: a `:state` declared inside a section belongs to that section, so two sections can both declare `count` without colliding. Bindings and buttons resolve to the nearest scope.
75
+
76
+ ## Reactive directives
77
+
78
+ Reactive pages opt into `/__wd/runtime.js` (under 1 KB gzipped). Static pages do not.
79
+
80
+ ```wd
81
+ :state count = 0
82
+
83
+ The count is { count }.
84
+
85
+ :button "Increment" -> count++
86
+
87
+ :if count
88
+ Count has changed.
89
+ :else
90
+ Count is still zero.
91
+ :endif
92
+
93
+ :state todos = [{"id": 1, "title": "Route pages"}]
94
+
95
+ @loop todos into todo
96
+ - { todo.title }
97
+ @endloop
98
+
99
+ :button "Add" -> todos += {"id": 2, "title": "Live compile"}
100
+ ```
101
+
102
+ Directive actions are intentionally narrow and compile-time checked. Arbitrary JavaScript belongs in a colocated `.js` file.
103
+
104
+ ## Data, forms, and persistence
105
+
106
+ - `:fetch name from "url"` declares state and fills it from JSON over the network. Shelf `.json` files are served at `/__wd/data/`.
107
+ - `:form into name` captures submits straight into state — no backend. `:form action="/url"` emits a plain native form with zero JS instead.
108
+ - `:input field placeholder="…" required` and `:submit "Label"` build the form body.
109
+ - `:state cart = [] persist` survives reloads via localStorage.
110
+ - `:if item.path` works inside reactive loops for per-row branches.
111
+ - 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`.
112
+
113
+ See it all live on the [Data & Forms page](/data/).
114
+
115
+ ## Colocation
116
+
117
+ - A matching `.skin` file attaches CSS to the page (indentation-based, compiles to real CSS).
118
+ - A matching `.js` file attaches page behavior.
119
+ - Both work for included fragments too, by basename.
120
+
121
+ ## Spec status
122
+
123
+ 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. `:fetch`, `:form`, `:session`, `:cart`, and server-driven fragments remain roadmap work — see `docs/spec-alignment.md` in the package for the full audit.
124
+
125
+ </main>
@@ -0,0 +1,10 @@
1
+ const button = document.querySelector("[data-wd-demo]");
2
+
3
+ if (button) {
4
+ button.addEventListener("click", () => {
5
+ button.classList.toggle("is-on");
6
+ button.textContent = button.classList.contains("is-on")
7
+ ? "Colocated index.js is active"
8
+ : "Script is waiting";
9
+ });
10
+ }
@@ -0,0 +1,134 @@
1
+ tokens
2
+ ink #171717
3
+ paper #fbfaf7
4
+ panel #ffffff
5
+ line #dedbd3
6
+ accent #16645a
7
+ warm #a83f2a
8
+ muted #666158
9
+ gap 1rem
10
+ radius 8px
11
+
12
+ page
13
+ margin 0
14
+ font ui-serif, Georgia, Cambria, serif
15
+ color $ink
16
+ bg $paper
17
+ line-height 1.6
18
+ padding 0 0 4rem
19
+
20
+ .hero
21
+ min-height 60vh
22
+ display flex
23
+ flex-direction column
24
+ align-items flex-start
25
+ justify-content center
26
+ border-bottom 1px solid $line
27
+ margin-bottom 2rem
28
+
29
+ .hero > p:not(.eyebrow)
30
+ max-width 70ch
31
+
32
+ .eyebrow
33
+ color $accent
34
+ text-transform uppercase
35
+ letter-spacing .08em
36
+ font-size .78rem
37
+ margin 0
38
+
39
+ main
40
+ max-width 920px
41
+ margin 0 auto
42
+ padding 1rem 2rem 2rem
43
+
44
+ h1
45
+ font-size clamp(2.4rem, 6vw, 5rem)
46
+ line-height .95
47
+ margin 2rem 0 1rem
48
+ max-width 10ch
49
+
50
+ h2
51
+ margin-top 2rem
52
+ border-bottom 1px solid $line
53
+ padding-bottom .35rem
54
+
55
+ a
56
+ color $accent
57
+
58
+ .btn
59
+ display inline-block
60
+ margin .5rem .5rem .5rem 0
61
+ padding .75rem 1rem
62
+ border 1px solid $line
63
+ radius $radius
64
+ bg $panel
65
+ text-decoration none
66
+
67
+ .btn.primary
68
+ bg $ink
69
+ color white
70
+ border 1px solid $ink
71
+
72
+ .note
73
+ border 1px solid $line
74
+ bg $panel
75
+ padding 1rem
76
+ radius $radius
77
+
78
+ .try-card
79
+ display inline-flex
80
+ align-items center
81
+ gap .65rem
82
+ margin .35rem .5rem .35rem 0
83
+ padding .75rem 1rem
84
+ border 1px solid $line
85
+ radius $radius
86
+ bg $panel
87
+ text-decoration none
88
+
89
+ .try-card span
90
+ color white
91
+ bg $accent
92
+ padding .1rem .45rem
93
+ radius 999px
94
+
95
+ .feature-card
96
+ display grid
97
+ gap .35rem
98
+ margin 1rem 0
99
+ padding 1rem
100
+ bg $panel
101
+ border 1px solid $line
102
+ radius $radius
103
+
104
+ .feature-card strong
105
+ color $warm
106
+
107
+ .pulse-button
108
+ border 0
109
+ bg $ink
110
+ color white
111
+ padding .85rem 1rem
112
+ radius $radius
113
+ font inherit
114
+ cursor pointer
115
+
116
+ .pulse-button.is-on
117
+ bg $warm
118
+
119
+ .sprint-board
120
+ display grid
121
+ grid-template-columns repeat(auto-fit, minmax(130px, 1fr))
122
+ gap $gap
123
+ margin 1rem 0
124
+
125
+ .sprint-board article
126
+ bg $panel
127
+ border 1px solid $line
128
+ padding 1rem
129
+ radius $radius
130
+
131
+ .sprint-board span
132
+ display block
133
+ color $accent
134
+ font-size .9rem
@@ -0,0 +1,64 @@
1
+ ---
2
+ title: Darkmown
3
+ ---
4
+
5
+ @include /nav.wd
6
+
7
+ <main>
8
+
9
+ <section class="hero">
10
+ <p class="eyebrow">Markdown-native framework</p>
11
+
12
+ # Darkmown
13
+
14
+ Markdown pages that compile into fast, mostly-static websites, with tiny reactive islands only where the document asks for them.
15
+
16
+ <a class="btn primary" href="/docs/">Read the docs</a>
17
+ <a class="btn" href="/reactive/">Try reactivity</a>
18
+ </section>
19
+
20
+ :note "Two formats, one rule: .md stays plain CommonMark forever, and renaming to .wd is what unlocks directives, includes, loops, and state."
21
+
22
+ ## What to try
23
+
24
+ :try "Open the docs route, a .wd page full of includes" href="/docs/"
25
+ :try "Open a plain .md page and see directives stay inert" href="/markdown/"
26
+ :try "Open the reactive page: state, keyed loops, scoped sections" href="/reactive/"
27
+ :try "Open the data page: :fetch, forms into state, persistence" href="/data/"
28
+ :try "Open a hidden route and confirm it 404s" href="/-draft/"
29
+
30
+ ## Loops that read like prose
31
+
32
+ `@loop` is the only loop. Point it at a JSON file, an in-scope value, or a `:state` list, and the body repeats once per row. Includes inside the loop inherit the loop value:
33
+
34
+ ```wd
35
+ @loop /features.json into card
36
+ @include /feature-card.wd
37
+ @endloop
38
+ ```
39
+
40
+ @loop /features.json into card
41
+ @include /feature-card.wd
42
+ @endloop
43
+
44
+ ## Colocated behavior
45
+
46
+ This button is powered by `site/pages/index.js`, discovered by matching the page basename:
47
+
48
+ <button class="pulse-button" data-wd-demo>Script is waiting</button>
49
+
50
+ ## Deploy in one click
51
+
52
+ This repo doubles as a template — `npm run build` emits a plain static `dist/`, so it deploys anywhere that serves files:
53
+
54
+ <a class="btn primary" href="https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FZVN-DEV%2Fdarkmown">Deploy to Vercel</a>
55
+ <a class="btn" href="https://deploy.workers.cloudflare.com/?url=https%3A%2F%2Fgithub.com%2FZVN-DEV%2Fdarkmown">Deploy to Cloudflare</a>
56
+ <a class="btn" href="https://github.com/ZVN-DEV/darkmown">Star on GitHub</a>
57
+
58
+ ## Sprint board
59
+
60
+ :sprint min=4 max=6 roles="PM, Development, Research, Testing, Review"
61
+
62
+ @include /sprint-loop.wd
63
+
64
+ </main>
@@ -0,0 +1,36 @@
1
+ ---
2
+ title: Plain Markdown
3
+ ---
4
+
5
+ # Plain Markdown stays plain
6
+
7
+ This page is `markdown.md`. Darkmown renders it with a real CommonMark parser and adds nothing — no runtime, no directives, no surprises.
8
+
9
+ ## Everything CommonMark works
10
+
11
+ 1. Ordered lists
12
+ 2. With proper numbering
13
+ 1. And nesting
14
+
15
+ > Blockquotes render correctly, including **inline emphasis**.
16
+
17
+ | Feature | Status |
18
+ | --- | --- |
19
+ | Tables | Working |
20
+ | Ordered lists | Working |
21
+ | Blockquotes | Working |
22
+ | Images | Working |
23
+
24
+ ![Darkmown logo](/__wd/media/logo.svg)
25
+
26
+ ## Directives stay inert here
27
+
28
+ The next line is `.wd` syntax, left as plain text on purpose:
29
+
30
+ :state count = 0
31
+
32
+ And `{ count }` braces in prose are just braces: { count }
33
+
34
+ Rename this file to `markdown.wd` and all of it comes alive. That is the whole upgrade path.
35
+
36
+ [Back home](/)
@@ -0,0 +1,63 @@
1
+ tokens
2
+ ink #171717
3
+ paper #fbfaf7
4
+ panel #ffffff
5
+ line #dedbd3
6
+ accent #16645a
7
+ radius 8px
8
+
9
+ html
10
+ bg $paper
11
+
12
+ page
13
+ max-width 760px
14
+ margin 0 auto
15
+ padding 3rem 2rem 5rem
16
+ font ui-serif, Georgia, Cambria, serif
17
+ color $ink
18
+ bg $paper
19
+ line-height 1.7
20
+
21
+ h1
22
+ font-size 3rem
23
+ line-height 1
24
+ margin 2rem 0 1rem
25
+
26
+ h2
27
+ margin-top 2.4rem
28
+ padding-top 1.2rem
29
+ border-top 1px solid $line
30
+
31
+ a
32
+ color $accent
33
+
34
+ code
35
+ bg $panel
36
+ border 1px solid $line
37
+ padding .08rem .28rem
38
+ radius 5px
39
+
40
+ blockquote
41
+ margin 1rem 0
42
+ padding .5rem 1.2rem
43
+ border-left 3px solid $accent
44
+ bg $panel
45
+ radius $radius
46
+
47
+ table
48
+ border-collapse collapse
49
+ bg $panel
50
+
51
+ th
52
+ text-align left
53
+ border 1px solid $line
54
+ padding .5rem .9rem
55
+ bg #f1ede4
56
+
57
+ td
58
+ border 1px solid $line
59
+ padding .5rem .9rem
60
+
61
+ img
62
+ width 64px
63
+ height 64px
@@ -0,0 +1,62 @@
1
+ tokens
2
+ ink #171717
3
+ paper #faf8f3
4
+ panel #ffffff
5
+ line #dedbd3
6
+ accent #16645a
7
+ warm #a83f2a
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
16
+
17
+ main
18
+ max-width 820px
19
+ margin 0 auto
20
+ padding 3rem 2rem 5rem
21
+
22
+ h1
23
+ font-size 3rem
24
+ line-height 1
25
+
26
+ h2
27
+ margin-top 2rem
28
+ border-top 1px solid $line
29
+ padding-top 1.2rem
30
+
31
+ button
32
+ border 1px solid $ink
33
+ bg $ink
34
+ color white
35
+ padding .72rem 1rem
36
+ margin .3rem .5rem .3rem 0
37
+ radius $radius
38
+ font inherit
39
+ cursor pointer
40
+
41
+ button:hover
42
+ bg $accent
43
+ border-color $accent
44
+
45
+ a
46
+ color $accent
47
+
48
+ ul
49
+ border 1px solid $line
50
+ bg $panel
51
+ padding 1rem 1rem 1rem 2rem
52
+ radius $radius
53
+
54
+ .tally-card
55
+ border 1px solid $line
56
+ bg $panel
57
+ padding .25rem 1.2rem 1.2rem
58
+ margin 1rem 0
59
+ radius $radius
60
+
61
+ .tally-card h3
62
+ color $warm