@zvndev/darkmown 1.3.0 → 1.5.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 +2 -0
- package/CHANGELOG.md +19 -0
- package/README.md +114 -1
- package/docs/spec-alignment.md +30 -0
- package/package.json +1 -1
- package/site/_/base.skin +231 -18
- package/site/_/nav.wd +1 -1
- package/site/_/scoped-promo.skin +43 -0
- package/site/_/scoped-promo.wd +10 -0
- package/site/_/scoped-stat.skin +32 -0
- package/site/_/scoped-stat.wd +9 -0
- package/site/pages/app.skin +27 -5
- package/site/pages/blog/_schema.wd +15 -0
- package/site/pages/blog/collections-and-pagination.md +38 -0
- package/site/pages/blog/drafts-that-never-leak.md +2 -0
- package/site/pages/blog/index.skin +144 -0
- package/site/pages/blog/index.wd +73 -0
- package/site/pages/blog/shipping-feeds.md +1 -0
- package/site/pages/blog/typed-frontmatter.md +45 -0
- package/site/pages/blog/zero-js-by-default.md +31 -0
- package/site/pages/data.skin +50 -9
- package/site/pages/docs/index.wd +70 -0
- package/site/pages/loops.skin +43 -6
- package/site/pages/reactive.skin +38 -6
- package/site/pages/scoped/index.skin +23 -0
- package/site/pages/scoped/index.wd +47 -0
- package/site/pages/store.skin +43 -6
- package/src/builder.js +213 -32
- package/src/compiler/collections.js +316 -0
- package/src/compiler/context.js +34 -2
- package/src/compiler/directives.js +11 -5
- package/src/compiler/includes.js +48 -1
- package/src/compiler/index.js +1 -1
- package/src/compiler/loops.js +130 -12
- package/src/compiler/page.js +93 -9
- package/src/compiler.js +2 -1
- package/src/skin.js +147 -3
- package/src/templates/blog/site/pages/index.skin +84 -0
- package/src/templates/dashboard/site/pages/index.skin +84 -0
- package/src/templates/landing/site/pages/index.skin +113 -6
- package/src/templates/starter/site/pages/index.skin +81 -3
- package/src/templates/store/site/pages/index.skin +93 -11
- package/site/pages/blog/index.md +0 -13
package/site/pages/app.skin
CHANGED
|
@@ -57,20 +57,42 @@ h1
|
|
|
57
57
|
color #9a2a2a
|
|
58
58
|
|
|
59
59
|
button
|
|
60
|
+
display inline-flex
|
|
61
|
+
align-items center
|
|
62
|
+
justify-content center
|
|
63
|
+
gap .4rem
|
|
60
64
|
background $accent
|
|
61
65
|
color white
|
|
62
|
-
border
|
|
63
|
-
padding .55rem
|
|
66
|
+
border 1px solid $accent
|
|
67
|
+
padding .55rem 1rem
|
|
64
68
|
margin .25rem .4rem .25rem 0
|
|
65
|
-
border-radius
|
|
66
|
-
font-size .
|
|
69
|
+
border-radius 999px
|
|
70
|
+
font-size .92rem
|
|
71
|
+
font-weight 560
|
|
67
72
|
cursor pointer
|
|
73
|
+
appearance none
|
|
74
|
+
transition transform .14s cubic-bezier(.2,.7,.3,1), box-shadow .14s ease
|
|
75
|
+
|
|
76
|
+
button:hover
|
|
77
|
+
transform translateY(-1px)
|
|
78
|
+
box-shadow 0 8px 18px -8px rgba(15,107,94,.5)
|
|
79
|
+
|
|
80
|
+
button:focus-visible
|
|
81
|
+
outline 2px solid $accent
|
|
82
|
+
outline-offset 2px
|
|
68
83
|
|
|
69
84
|
input
|
|
70
85
|
font-size 1rem
|
|
71
|
-
padding .
|
|
86
|
+
padding .55rem .75rem
|
|
72
87
|
border 1px solid $line
|
|
73
88
|
border-radius 8px
|
|
74
89
|
width 100%
|
|
75
90
|
max-width 22rem
|
|
76
91
|
margin .3rem 0
|
|
92
|
+
appearance none
|
|
93
|
+
transition border-color .14s ease, box-shadow .14s ease
|
|
94
|
+
|
|
95
|
+
input:focus-visible
|
|
96
|
+
outline none
|
|
97
|
+
border-color $accent
|
|
98
|
+
box-shadow 0 0 0 3px rgba(15,107,94,.18)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
# Typed schema for the blog collection. Every post under site/pages/blog/ is
|
|
3
|
+
# validated against these rules at build time — a missing required field, a
|
|
4
|
+
# wrong type, or an unknown extra field fails the build with a file:line. Append
|
|
5
|
+
# "?" to a type to make the field optional. Remove this file to turn validation
|
|
6
|
+
# off (it is opt-in).
|
|
7
|
+
title: string
|
|
8
|
+
date: date
|
|
9
|
+
description: string
|
|
10
|
+
excerpt: string?
|
|
11
|
+
tags: string[]?
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
The blog collection schema. Posts are plain `.md` files next to this file;
|
|
15
|
+
their frontmatter must match the rules above.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Collections and pagination, with zero ceremony
|
|
3
|
+
description: Any folder under site/pages is a queryable collection. Loop it by name, sort it, paginate it — all at build time, all zero-JS.
|
|
4
|
+
date: 2026-06-28
|
|
5
|
+
excerpt: This very page is an entry in a collection. The list you came from is one @loop over a folder — no content root, no marker file, no config.
|
|
6
|
+
tags: [collections, content]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Collections and pagination, with zero ceremony
|
|
10
|
+
|
|
11
|
+
The page you came from is not a hand-maintained list of links. It is one
|
|
12
|
+
`@loop` over a *collection* — and a collection is just a folder. Every `.md`
|
|
13
|
+
and `.wd` file under `site/pages/blog/` is an entry, queryable by the folder's
|
|
14
|
+
bare name:
|
|
15
|
+
|
|
16
|
+
```wd
|
|
17
|
+
@loop blog where draft == false sort by date desc paginate 3 into post
|
|
18
|
+
- [{ post.title }]({ post.url }) — { post.date }
|
|
19
|
+
@endloop
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
No `content/` root to opt into, no marker file to drop in, no config object.
|
|
23
|
+
A folder of posts *is* the collection. Each entry's frontmatter becomes a row,
|
|
24
|
+
plus three fields the framework derives for you: `url` (the entry's route),
|
|
25
|
+
`slug` (its filename), and `excerpt` (the post's first paragraph when you don't
|
|
26
|
+
write one).
|
|
27
|
+
|
|
28
|
+
## It is the same loop you already know
|
|
29
|
+
|
|
30
|
+
There is no second loop syntax. `where`, `sort by`, `reverse`, `offset`,
|
|
31
|
+
`limit`, and the format pipes all work over a collection exactly as they do over
|
|
32
|
+
a JSON file — because under the hood a collection resolves to the same array of
|
|
33
|
+
rows. The only new word is `paginate N`, which slices the listing into static
|
|
34
|
+
pages: page one keeps this listing's clean route, and pages two and up live at
|
|
35
|
+
`/blog/page/2/`, `/blog/page/3/`, and so on. Every one is plain HTML.
|
|
36
|
+
|
|
37
|
+
A pure listing like this ships **zero** framework JavaScript. The pager links —
|
|
38
|
+
newer, older, numbered — are just `<a href>`s to the generated routes.
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
title: Drafts that never leak
|
|
3
3
|
description: draft true excludes a page everywhere in a production build — dist, routes.json, sitemap, and rss.
|
|
4
4
|
date: 2026-06-27
|
|
5
|
+
excerpt: Set draft true on a post and it vanishes from a production build — dist, routes.json, the sitemap, and the feed — even with a date.
|
|
6
|
+
tags: [drafts, workflow]
|
|
5
7
|
---
|
|
6
8
|
|
|
7
9
|
# Drafts that never leak
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/* The Darkmown blog index — a collection-driven, paginated listing. Thin layer
|
|
2
|
+
over /base.wd (tokens, typography, dark mode); only the post-card grid and the
|
|
3
|
+
pager live here. Namespaced under .app-blog so it can't leak. */
|
|
4
|
+
|
|
5
|
+
.app-blog
|
|
6
|
+
max-width 52rem
|
|
7
|
+
|
|
8
|
+
.app-blog .blog-head
|
|
9
|
+
margin 2.5rem 0 2rem
|
|
10
|
+
|
|
11
|
+
.app-blog .blog-head .eyebrow
|
|
12
|
+
display inline-block
|
|
13
|
+
font-size .78rem
|
|
14
|
+
font-weight 600
|
|
15
|
+
text-transform uppercase
|
|
16
|
+
letter-spacing .12em
|
|
17
|
+
color var(--accent)
|
|
18
|
+
margin-bottom .6rem
|
|
19
|
+
|
|
20
|
+
.app-blog .blog-head h1
|
|
21
|
+
margin .2rem 0 .8rem
|
|
22
|
+
line-height 1.1
|
|
23
|
+
|
|
24
|
+
.app-blog .blog-head p
|
|
25
|
+
color var(--muted)
|
|
26
|
+
max-width 38rem
|
|
27
|
+
|
|
28
|
+
/* The list of posts. */
|
|
29
|
+
.app-blog .post-list
|
|
30
|
+
display flex
|
|
31
|
+
flex-direction column
|
|
32
|
+
gap 1.1rem
|
|
33
|
+
margin 2rem 0
|
|
34
|
+
|
|
35
|
+
.app-blog .post-card
|
|
36
|
+
background var(--panel)
|
|
37
|
+
border 1px solid var(--line)
|
|
38
|
+
radius var(--radius-lg)
|
|
39
|
+
padding 1.5rem 1.6rem
|
|
40
|
+
transition border-color .15s ease, transform .15s ease, box-shadow .15s ease
|
|
41
|
+
|
|
42
|
+
.app-blog .post-card:hover
|
|
43
|
+
border-color var(--accent)
|
|
44
|
+
transform translateY(-2px)
|
|
45
|
+
box-shadow 0 8px 24px -16px var(--accent-deep)
|
|
46
|
+
|
|
47
|
+
.app-blog .post-meta
|
|
48
|
+
display flex
|
|
49
|
+
align-items center
|
|
50
|
+
flex-wrap wrap
|
|
51
|
+
gap .55rem
|
|
52
|
+
margin-bottom .55rem
|
|
53
|
+
|
|
54
|
+
/* Each meta item (time / tag) renders as its own paragraph; strip the default
|
|
55
|
+
block margins so they sit inline in the flex row. */
|
|
56
|
+
.app-blog .post-meta p
|
|
57
|
+
margin 0
|
|
58
|
+
display inline-flex
|
|
59
|
+
|
|
60
|
+
.app-blog .post-meta time
|
|
61
|
+
font-size .82rem
|
|
62
|
+
font-weight 600
|
|
63
|
+
color var(--muted)
|
|
64
|
+
font-variant-numeric tabular-nums
|
|
65
|
+
|
|
66
|
+
.app-blog .tag
|
|
67
|
+
font-size .72rem
|
|
68
|
+
font-weight 600
|
|
69
|
+
text-transform uppercase
|
|
70
|
+
letter-spacing .06em
|
|
71
|
+
color var(--accent-deep)
|
|
72
|
+
background var(--accent-soft)
|
|
73
|
+
border 1px solid color-mix(in srgb, var(--accent) 30%, transparent)
|
|
74
|
+
padding .15rem .5rem
|
|
75
|
+
radius 999px
|
|
76
|
+
|
|
77
|
+
.app-blog .post-card h3
|
|
78
|
+
margin .1rem 0 .5rem
|
|
79
|
+
font-size 1.4rem
|
|
80
|
+
line-height 1.2
|
|
81
|
+
|
|
82
|
+
.app-blog .post-card h3 a
|
|
83
|
+
color var(--ink)
|
|
84
|
+
text-decoration none
|
|
85
|
+
|
|
86
|
+
.app-blog .post-card h3 a:hover
|
|
87
|
+
color var(--accent)
|
|
88
|
+
|
|
89
|
+
.app-blog .post-excerpt
|
|
90
|
+
color var(--ink-soft)
|
|
91
|
+
margin 0 0 .8rem
|
|
92
|
+
line-height 1.55
|
|
93
|
+
|
|
94
|
+
.app-blog .post-more a
|
|
95
|
+
font-size .88rem
|
|
96
|
+
font-weight 600
|
|
97
|
+
color var(--accent)
|
|
98
|
+
text-decoration none
|
|
99
|
+
|
|
100
|
+
.app-blog .post-more a:hover
|
|
101
|
+
text-decoration underline
|
|
102
|
+
|
|
103
|
+
.app-blog .empty-note
|
|
104
|
+
color var(--muted)
|
|
105
|
+
font-style italic
|
|
106
|
+
padding 2rem
|
|
107
|
+
text-align center
|
|
108
|
+
border 1px dashed var(--line)
|
|
109
|
+
radius var(--radius)
|
|
110
|
+
|
|
111
|
+
/* The pager — plain links to the generated routes, zero JS. */
|
|
112
|
+
.app-blog .pager
|
|
113
|
+
display flex
|
|
114
|
+
align-items center
|
|
115
|
+
justify-content space-between
|
|
116
|
+
gap 1rem
|
|
117
|
+
margin 2.5rem 0 3rem
|
|
118
|
+
padding-top 1.4rem
|
|
119
|
+
border-top 1px solid var(--line)
|
|
120
|
+
|
|
121
|
+
.app-blog .pager-side
|
|
122
|
+
flex 1
|
|
123
|
+
min-width 0
|
|
124
|
+
|
|
125
|
+
.app-blog .pager-side p
|
|
126
|
+
margin 0
|
|
127
|
+
|
|
128
|
+
.app-blog .pager-side--end
|
|
129
|
+
text-align right
|
|
130
|
+
|
|
131
|
+
.app-blog .pager-now
|
|
132
|
+
font-size .85rem
|
|
133
|
+
font-weight 600
|
|
134
|
+
color var(--muted)
|
|
135
|
+
white-space nowrap
|
|
136
|
+
font-variant-numeric tabular-nums
|
|
137
|
+
|
|
138
|
+
.app-blog .pager-link
|
|
139
|
+
font-weight 600
|
|
140
|
+
color var(--accent)
|
|
141
|
+
text-decoration none
|
|
142
|
+
|
|
143
|
+
.app-blog .pager-link:hover
|
|
144
|
+
text-decoration underline
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: The Darkmown Blog
|
|
3
|
+
description: Notes on shipping Markdown that runs — collections, feeds, drafts, and zero-JS releases. This whole index is one @loop over a folder.
|
|
4
|
+
image: https://darkmown.com/__wd/media/og.png
|
|
5
|
+
transitions: true
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
@include /nav.wd
|
|
9
|
+
@include /base.wd
|
|
10
|
+
|
|
11
|
+
<main class="wide app-blog">
|
|
12
|
+
|
|
13
|
+
<header class="blog-head">
|
|
14
|
+
<span class="eyebrow">The Darkmown Blog</span>
|
|
15
|
+
|
|
16
|
+
# Notes from the framework
|
|
17
|
+
|
|
18
|
+
Every post below is a plain `.md` file in `site/pages/blog/`. This page is **one
|
|
19
|
+
`@loop` over that folder** — sorted newest-first, paginated, and 100% static
|
|
20
|
+
HTML. No hand-maintained list, no config, no JavaScript.
|
|
21
|
+
|
|
22
|
+
</header>
|
|
23
|
+
|
|
24
|
+
<section class="post-list">
|
|
25
|
+
|
|
26
|
+
@loop blog into post sort by post.date desc paginate 3
|
|
27
|
+
::: card .post-card
|
|
28
|
+
::: post-meta
|
|
29
|
+
<time>{ post.date }</time>
|
|
30
|
+
@loop post.tags into tag
|
|
31
|
+
<span class="tag">{ tag }</span>
|
|
32
|
+
@endloop
|
|
33
|
+
:::
|
|
34
|
+
|
|
35
|
+
### [{ post.title }]({ post.url })
|
|
36
|
+
|
|
37
|
+
::: post-excerpt
|
|
38
|
+
{ post.excerpt }
|
|
39
|
+
:::
|
|
40
|
+
|
|
41
|
+
::: post-more
|
|
42
|
+
[Read post → ]({ post.url })
|
|
43
|
+
:::
|
|
44
|
+
:::
|
|
45
|
+
@empty
|
|
46
|
+
::: empty-note
|
|
47
|
+
No posts yet. Add a dated `.md` file to `site/pages/blog/`.
|
|
48
|
+
:::
|
|
49
|
+
@endloop
|
|
50
|
+
|
|
51
|
+
</section>
|
|
52
|
+
|
|
53
|
+
::: pager
|
|
54
|
+
::: pager-side
|
|
55
|
+
:if page.prev
|
|
56
|
+
[← Newer posts]({ page.prev }){.pager-link}
|
|
57
|
+
:endif
|
|
58
|
+
:::
|
|
59
|
+
|
|
60
|
+
::: pager-now
|
|
61
|
+
Page { page.current } of { page.total }
|
|
62
|
+
:::
|
|
63
|
+
|
|
64
|
+
::: pager-side .pager-side--end
|
|
65
|
+
:if page.next
|
|
66
|
+
[Older posts →]({ page.next }){.pager-link}
|
|
67
|
+
:endif
|
|
68
|
+
:::
|
|
69
|
+
:::
|
|
70
|
+
|
|
71
|
+
</main>
|
|
72
|
+
|
|
73
|
+
@include /footer.wd
|
|
@@ -3,6 +3,7 @@ title: Shipping feeds without a config file
|
|
|
3
3
|
description: Site identity lives in the home page frontmatter — one site_url field turns on sitemap.xml and rss.xml.
|
|
4
4
|
date: 2026-06-26
|
|
5
5
|
excerpt: One reserved frontmatter field on the home page — site_url — turns on the whole feeds story. No config loader, no plugin.
|
|
6
|
+
tags: [feeds, seo]
|
|
6
7
|
---
|
|
7
8
|
|
|
8
9
|
# Shipping feeds without a config file
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Typed frontmatter with a four-line schema
|
|
3
|
+
description: Drop a _schema.wd in a collection and every entry is validated at build time — missing fields, wrong types, and typos all fail the build.
|
|
4
|
+
date: 2026-06-29
|
|
5
|
+
excerpt: A typo'd frontmatter key used to ship silently. With a collection schema, titel instead of title fails the build with a file and line.
|
|
6
|
+
tags: [collections, schema, dx]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Typed frontmatter with a four-line schema
|
|
10
|
+
|
|
11
|
+
Frontmatter is convenient until a typo bites. You write `titel:` instead of
|
|
12
|
+
`title:`, the field silently becomes empty, and the bug surfaces as a blank
|
|
13
|
+
heading in production. Darkmown closes that gap with an *optional* schema — one
|
|
14
|
+
file at the collection root, frontmatter-shaped, written exactly like the
|
|
15
|
+
frontmatter it checks:
|
|
16
|
+
|
|
17
|
+
```wd
|
|
18
|
+
---
|
|
19
|
+
title: string
|
|
20
|
+
date: date
|
|
21
|
+
description: string
|
|
22
|
+
excerpt: string?
|
|
23
|
+
tags: string[]?
|
|
24
|
+
---
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The vocabulary is small and closed on purpose: `string`, `number`, `boolean`,
|
|
28
|
+
`date`, and `string[]`, with a trailing `?` to make a field optional. That is
|
|
29
|
+
the whole DSL. An unknown type token is a compile error in the schema file
|
|
30
|
+
itself.
|
|
31
|
+
|
|
32
|
+
## What it catches
|
|
33
|
+
|
|
34
|
+
At build time, every entry in the collection is checked against the rules. A
|
|
35
|
+
missing required field, a value of the wrong type, or an extra field the schema
|
|
36
|
+
never declared each fails the build — and the error names the offending entry
|
|
37
|
+
by path and the field by name, so the fix is obvious:
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
Missing required field "date" (date) in /blog/example/ (collection "blog").
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Validation is opt-in. No `_schema.wd`, no validation — the same posture as
|
|
44
|
+
`scoped` styles and the `draft` flag. Add the file when you want the guardrails;
|
|
45
|
+
leave it out while you are still sketching.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Zero JavaScript, by default
|
|
3
|
+
description: A Darkmown page ships no framework JavaScript unless it declares reactive behavior. The blog you are reading proves it.
|
|
4
|
+
date: 2026-06-25
|
|
5
|
+
excerpt: View source on this page. There is no runtime script tag, because nothing on it is reactive — and the framework knows the difference.
|
|
6
|
+
tags: [performance, philosophy]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Zero JavaScript, by default
|
|
10
|
+
|
|
11
|
+
Open your browser's view-source on this post. Look for a `<script>` that loads a
|
|
12
|
+
framework runtime. There isn't one — and there isn't one on the blog index, or
|
|
13
|
+
on any of its paginated pages either. None of them declare reactive behavior, so
|
|
14
|
+
none of them pay for it.
|
|
15
|
+
|
|
16
|
+
That is the whole bargain. A `.md` file is plain CommonMark. Rename it to `.wd`
|
|
17
|
+
and you unlock directives — state, loops, conditionals — but you only ship the
|
|
18
|
+
~7.4 KB runtime on the pages that actually use them. A listing built from a
|
|
19
|
+
collection loop resolves entirely at build time, so it stays static HTML.
|
|
20
|
+
|
|
21
|
+
## The extension is the feature gate
|
|
22
|
+
|
|
23
|
+
There is no flag to toggle, no "static export" mode to remember. The compiler
|
|
24
|
+
tracks whether a page declared anything reactive and emits `runtime: false` into
|
|
25
|
+
`routes.json` when it didn't. A collection listing, a paginated archive, a page
|
|
26
|
+
of prose with a code block — all static, all cacheable on a CDN forever, all
|
|
27
|
+
fast on the first byte because there is nothing to hydrate.
|
|
28
|
+
|
|
29
|
+
When you do want an island of interactivity — a search box, a cart, a sortable
|
|
30
|
+
table — you declare it, and only that page loads the runtime. The rest of the
|
|
31
|
+
site stays weightless.
|
package/site/pages/data.skin
CHANGED
|
@@ -29,18 +29,44 @@ h2
|
|
|
29
29
|
padding-top 1.2rem
|
|
30
30
|
|
|
31
31
|
button
|
|
32
|
-
|
|
33
|
-
|
|
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 .
|
|
39
|
+
padding .6rem 1.1rem
|
|
36
40
|
margin .3rem .5rem .3rem 0
|
|
37
|
-
radius
|
|
38
|
-
font
|
|
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
|
|
@@ -75,21 +101,36 @@ form
|
|
|
75
101
|
margin 1rem 0
|
|
76
102
|
|
|
77
103
|
input
|
|
78
|
-
padding .
|
|
104
|
+
padding .62rem .85rem
|
|
79
105
|
border 1px solid $line
|
|
80
106
|
radius $radius
|
|
81
107
|
font inherit
|
|
108
|
+
font-size .95rem
|
|
82
109
|
bg $panel
|
|
110
|
+
appearance none
|
|
111
|
+
transition border-color .14s ease, box-shadow .14s ease
|
|
112
|
+
|
|
113
|
+
input:hover
|
|
114
|
+
border-color $accent
|
|
115
|
+
|
|
116
|
+
input:focus-visible
|
|
117
|
+
outline none
|
|
118
|
+
border-color $accent
|
|
119
|
+
box-shadow 0 0 0 3px rgba(22,100,90,.18)
|
|
83
120
|
|
|
84
121
|
.pulse-button
|
|
85
|
-
border
|
|
122
|
+
border 1px solid $warm
|
|
86
123
|
bg $warm
|
|
87
124
|
color white
|
|
88
|
-
padding .
|
|
89
|
-
radius
|
|
125
|
+
padding .6rem 1.1rem
|
|
126
|
+
radius 999px
|
|
90
127
|
font inherit
|
|
91
128
|
cursor pointer
|
|
92
129
|
|
|
130
|
+
.pulse-button:hover
|
|
131
|
+
transform translateY(-1px)
|
|
132
|
+
box-shadow 0 8px 18px -8px rgba(168,63,42,.5)
|
|
133
|
+
|
|
93
134
|
.tally-card
|
|
94
135
|
border 1px solid $line
|
|
95
136
|
bg $panel
|
package/site/pages/docs/index.wd
CHANGED
|
@@ -148,6 +148,53 @@ Nothing left to do.
|
|
|
148
148
|
|
|
149
149
|
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
150
|
|
|
151
|
+
## Content collections
|
|
152
|
+
|
|
153
|
+
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.
|
|
154
|
+
|
|
155
|
+
```wd
|
|
156
|
+
@loop blog into post sort by post.date desc
|
|
157
|
+
- [{ post.title }]({ post.url }) — { post.date }
|
|
158
|
+
@endloop
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
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`).
|
|
162
|
+
|
|
163
|
+
### Typed schema — `_schema.wd`
|
|
164
|
+
|
|
165
|
+
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:
|
|
166
|
+
|
|
167
|
+
```wd
|
|
168
|
+
---
|
|
169
|
+
title: string
|
|
170
|
+
date: date
|
|
171
|
+
excerpt: string?
|
|
172
|
+
tags: string[]?
|
|
173
|
+
---
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
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.
|
|
177
|
+
|
|
178
|
+
### Pagination — `paginate N`
|
|
179
|
+
|
|
180
|
+
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.
|
|
181
|
+
|
|
182
|
+
```wd
|
|
183
|
+
@loop blog into post sort by post.date desc paginate 5
|
|
184
|
+
- [{ post.title }]({ post.url })
|
|
185
|
+
@endloop
|
|
186
|
+
|
|
187
|
+
Page { page.current } of { page.total }
|
|
188
|
+
:if page.prev
|
|
189
|
+
[← Newer]({ page.prev })
|
|
190
|
+
:endif
|
|
191
|
+
:if page.next
|
|
192
|
+
[Older →]({ page.next })
|
|
193
|
+
:endif
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
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).
|
|
197
|
+
|
|
151
198
|
## Interpolation
|
|
152
199
|
|
|
153
200
|
One syntax everywhere: `{ name }` or `{ name.path }`.
|
|
@@ -414,6 +461,29 @@ For an explicit light/dark switch alongside the OS preference, declare `:theme`
|
|
|
414
461
|
|
|
415
462
|
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/).
|
|
416
463
|
|
|
464
|
+
## Scoped styles — `scoped`
|
|
465
|
+
|
|
466
|
+
A colocated `.skin` is **global** by default. Make its **first line** `scoped` and its selectors only ever match the component it ships with — so two components can both use `.card` without colliding. It's pure compile time: a path-derived id (e.g. `wd-7c21`) is stamped onto the HTML and appended to each selector.
|
|
467
|
+
|
|
468
|
+
```skin
|
|
469
|
+
scoped
|
|
470
|
+
|
|
471
|
+
.card
|
|
472
|
+
padding 1.5rem
|
|
473
|
+
bg $panel
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
compiles to `.card[data-wd-scope="wd-7c21"] { … }`, and the component's markup is stamped `data-wd-scope="wd-7c21"`. You still write `class="card"` — no renaming, no runtime, **zero JS** on a static page. A page skin scopes the page body; an **include** skin scopes just that include's subtree wherever it's used.
|
|
477
|
+
|
|
478
|
+
| In a `scoped` skin | Result |
|
|
479
|
+
|---|---|
|
|
480
|
+
| A selector rule | Scoped — attribute on the subject, before any `:hover`/`::before` |
|
|
481
|
+
| `tokens` / `tokens dark` | **Global** on `:root` — `$vars` and dark mode keep working site-wide |
|
|
482
|
+
| `:global(.toast)` (whole selector) | Opts back out — plain unscoped `.toast { … }` |
|
|
483
|
+
| `page` / `*` / `html` / `body` / `::selection` | Compile error — page-level styles belong in a global skin |
|
|
484
|
+
|
|
485
|
+
Honest limits this release: only **whole-selector** `:global()` (no `.card :global(.x)` yet), and an **unused** scoped selector **warns but is not removed** (a colocated `.js` may add the class). See the side-by-side [Scoped demo](/scoped/).
|
|
486
|
+
|
|
417
487
|
## Media — `:video`, `:audio`, `:embed`
|
|
418
488
|
|
|
419
489
|
Three compile-time-only directives (they emit no `data-wd-*`, so a media page stays zero-JS):
|
package/site/pages/loops.skin
CHANGED
|
@@ -32,28 +32,65 @@ a
|
|
|
32
32
|
color $accent
|
|
33
33
|
|
|
34
34
|
button
|
|
35
|
-
|
|
36
|
-
|
|
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 .
|
|
42
|
+
padding .6rem 1.1rem
|
|
39
43
|
margin .3rem .5rem .3rem 0
|
|
40
|
-
radius
|
|
41
|
-
font
|
|
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 .
|
|
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
|