@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
package/src/statics.js CHANGED
@@ -1,19 +1,67 @@
1
1
  import fs from "node:fs";
2
2
  import http from "node:http";
3
3
  import path from "node:path";
4
- import { BASE_SECURITY_HEADERS, REACTIVE_CSP } from "./headers.js";
4
+ import { BASE_SECURITY_HEADERS, REACTIVE_CSP, STATIC_CSP } from "./headers.js";
5
5
 
6
6
  /**
7
- * Security headers for an HTML response from the local server. The relaxed
8
- * (reactive) CSP is a superset that satisfies both static and reactive pages,
9
- * so the runtime server uses it for every HTML response; the build-time
10
- * outputs (`vercel.json`, `dist/_headers`) tighten static routes per-path.
11
- * @type {Record<string, string>}
7
+ * The security headers (including the right CSP variant) for an HTML response.
8
+ * Mirrors what production ships: a route the build marked reactive gets the
9
+ * relaxed CSP (`'unsafe-eval'` for `new Function`); a static route gets the
10
+ * stricter, eval-free CSP — so a local preview matches the per-route `_headers`
11
+ * / `vercel.json` the same build writes instead of blanket-relaxing every page.
12
+ * Anything not found in `routes.json` (an unknown route, a 404, a missing
13
+ * manifest) falls back to the reactive CSP, exactly like the production
14
+ * `_headers` catch-all block.
15
+ * @param {string} distRoot Absolute path to the build output directory.
16
+ * @param {string} url Request URL (may include a query string).
17
+ * @returns {Record<string, string>}
12
18
  */
13
- const HTML_SECURITY_HEADERS = {
14
- ...BASE_SECURITY_HEADERS,
15
- "Content-Security-Policy": REACTIVE_CSP
16
- };
19
+ function htmlSecurityHeaders(distRoot, url) {
20
+ return {
21
+ ...BASE_SECURITY_HEADERS,
22
+ "Content-Security-Policy": routeIsReactive(distRoot, routeOf(url)) ? REACTIVE_CSP : STATIC_CSP
23
+ };
24
+ }
25
+
26
+ /**
27
+ * Normalize a request URL to its clean, trailing-slashed route path so it can be
28
+ * matched against `routes.json` (`/docs` / `/docs/` / `/docs/index.html` all →
29
+ * `/docs/`). Returns null for an unparseable URL.
30
+ * @param {string} url
31
+ * @returns {string | null}
32
+ */
33
+ function routeOf(url) {
34
+ let pathname;
35
+ try {
36
+ pathname = decodeURIComponent((url || "/").split("?")[0]);
37
+ } catch {
38
+ return null;
39
+ }
40
+ if (pathname.endsWith("/index.html")) pathname = pathname.slice(0, -"index.html".length);
41
+ if (!pathname.endsWith("/")) pathname += "/";
42
+ return pathname;
43
+ }
44
+
45
+ /**
46
+ * Whether the route ships the reactive runtime, per the build's `routes.json`.
47
+ * Defaults to `true` (reactive) when the route is absent or the manifest can't be
48
+ * read — matching the production `_headers` catch-all, so an unknown route is
49
+ * never accidentally served the stricter policy.
50
+ * @param {string} distRoot
51
+ * @param {string | null} route
52
+ * @returns {boolean}
53
+ */
54
+ function routeIsReactive(distRoot, route) {
55
+ if (!route) return true;
56
+ try {
57
+ const manifest = JSON.parse(fs.readFileSync(path.join(distRoot, "routes.json"), "utf8"));
58
+ const entry = manifest.find((/** @type {{ route: string }} */ e) => e.route === route);
59
+ if (entry) return Boolean(entry.assets?.runtime);
60
+ } catch {
61
+ /* no/unreadable manifest → reactive default (the catch-all policy) */
62
+ }
63
+ return true;
64
+ }
17
65
 
18
66
  /**
19
67
  * Serve a built file for a request URL out of `distRoot`, or a 404 page.
@@ -25,7 +73,10 @@ const HTML_SECURITY_HEADERS = {
25
73
  export function serve(distRoot, url, res) {
26
74
  const file = resolvePublicFile(distRoot, url || "/");
27
75
  if (!file || !fs.existsSync(file)) {
28
- res.writeHead(404, { "content-type": "text/html; charset=utf-8", ...HTML_SECURITY_HEADERS });
76
+ res.writeHead(404, {
77
+ "content-type": "text/html; charset=utf-8",
78
+ ...htmlSecurityHeaders(distRoot, url)
79
+ });
29
80
  const notFound = path.join(distRoot, "404.html");
30
81
  if (fs.existsSync(notFound)) {
31
82
  res.end(fs.readFileSync(notFound));
@@ -36,7 +87,7 @@ export function serve(distRoot, url, res) {
36
87
  }
37
88
  const type = contentType(file);
38
89
  const headers = type.startsWith("text/html")
39
- ? { "content-type": type, ...HTML_SECURITY_HEADERS }
90
+ ? { "content-type": type, ...htmlSecurityHeaders(distRoot, url) }
40
91
  : { "content-type": type };
41
92
  res.writeHead(200, headers);
42
93
  fs.createReadStream(file).pipe(res);
@@ -1,12 +1,21 @@
1
1
  # Blog — a Darkmown template
2
2
 
3
- A markdown-native blog. Posts are plain `.md` files in `site/pages/posts/`; the
4
- home page loops `site/_/posts.json` to list them. Static pages, zero JavaScript.
3
+ A markdown-native blog. Posts are plain `.md` files in `site/pages/posts/` —
4
+ that folder is a typed content collection, and the home page is one `@loop`
5
+ over it. No manifest, no config. Static pages, zero JavaScript.
5
6
 
6
7
  ## Add a post
7
8
 
8
- 1. Create `site/pages/posts/<slug>.md` with a `title` in frontmatter.
9
- 2. Add an entry to `site/_/posts.json` (`slug`, `url`, `title`, `date`, `excerpt`).
9
+ 1. Create `site/pages/posts/<slug>.md` with `title`, `date`, and `description`
10
+ in frontmatter (`excerpt` is optional). That's it — the home page lists it
11
+ automatically, newest first, and dated posts land in `rss.xml`.
12
+ 2. Optional: copy an existing post's `.skin` beside the new file — a colocated
13
+ `.skin` attaches automatically, keeping the reading design.
14
+
15
+ `site/pages/posts/_schema.wd` type-checks every post's frontmatter at build
16
+ time, so a typo'd or missing field fails the build with a file and line. When
17
+ the list grows, add `paginate 10` to the loop in `site/pages/index.wd` to split
18
+ it into `/page/2/`, `/page/3/`, … automatically.
10
19
 
11
20
  ## Commands
12
21
 
@@ -1,3 +1,7 @@
1
+ ---
2
+ html: true
3
+ ---
4
+
1
5
  <nav>
2
6
  <strong>My Blog</strong>
3
7
  <span class="links">
@@ -1,8 +1,23 @@
1
1
  tokens
2
2
  ink #1d2530
3
3
  paper #fbfaf7
4
+ panel #ffffff
5
+ line #e7e3da
4
6
  muted #6b7785
5
7
  accent #2f6df0
8
+ accent-deep #2356cc
9
+ ring #2f6df0
10
+ radius 10px
11
+
12
+ tokens dark
13
+ ink #e7ecf3
14
+ paper #0f141b
15
+ panel #161d27
16
+ line #28303c
17
+ muted #93a0b2
18
+ accent #5b8cff
19
+ accent-deep #80a6ff
20
+ ring #5b8cff
6
21
 
7
22
  page
8
23
  margin 0
@@ -10,6 +25,7 @@ page
10
25
  color $ink
11
26
  bg $paper
12
27
  line-height 1.6
28
+ -webkit-font-smoothing antialiased
13
29
 
14
30
  main
15
31
  max-width 720px
@@ -49,3 +65,74 @@ nav a
49
65
  margin 0 0 .5rem
50
66
  color $muted
51
67
  font-size .9rem
68
+
69
+ .post-meta p
70
+ margin 0
71
+
72
+ /* Buttons & form controls — for newsletter signups, :button actions, etc. */
73
+ button
74
+ display inline-flex
75
+ align-items center
76
+ justify-content center
77
+ gap .5rem
78
+ bg $accent
79
+ color white
80
+ border 1px solid $accent
81
+ padding .6rem 1.15rem
82
+ margin .4rem .5rem .4rem 0
83
+ radius 999px
84
+ font-size .94rem
85
+ font-weight 560
86
+ line-height 1.15
87
+ cursor pointer
88
+ appearance none
89
+ box-shadow 0 1px 2px rgba(29,37,48,.08)
90
+ transition transform .14s cubic-bezier(.2,.7,.3,1), box-shadow .14s ease, background .14s ease, border-color .14s ease
91
+
92
+ button:hover
93
+ bg $accent-deep
94
+ border-color $accent-deep
95
+ transform translateY(-1px)
96
+ box-shadow 0 8px 18px -8px rgba(47,109,240,.55)
97
+
98
+ button:active
99
+ transform translateY(0)
100
+ box-shadow 0 1px 2px rgba(29,37,48,.08)
101
+
102
+ button:focus-visible
103
+ outline 2px solid $ring
104
+ outline-offset 2px
105
+
106
+ button:disabled
107
+ opacity .5
108
+ cursor not-allowed
109
+ box-shadow none
110
+ transform none
111
+
112
+ input, textarea, select
113
+ font inherit
114
+ font-size .95rem
115
+ color $ink
116
+ bg $panel
117
+ border 1px solid $line
118
+ radius $radius
119
+ padding .58rem .8rem
120
+ margin .35rem 0
121
+ width 100%
122
+ max-width 24rem
123
+ appearance none
124
+ transition border-color .14s ease, box-shadow .14s ease
125
+
126
+ input:hover, textarea:hover, select:hover
127
+ border-color $accent
128
+
129
+ input:focus-visible, textarea:focus-visible, select:focus-visible
130
+ outline none
131
+ border-color $accent
132
+ box-shadow 0 0 0 3px rgba(47,109,240,.18)
133
+
134
+ @media (prefers-reduced-motion: reduce)
135
+ button
136
+ transition none
137
+ button:hover
138
+ transform none
@@ -1,4 +1,5 @@
1
1
  ---
2
+ html: true
2
3
  title: My Blog
3
4
  description: A markdown-native blog built with Darkmown.
4
5
  site_url: https://example.com
@@ -11,15 +12,20 @@ transitions: true
11
12
 
12
13
  # My Blog
13
14
 
14
- Posts are plain Markdown files in `site/pages/posts/`. This index is one `.wd`
15
- loop over a small JSON manifest — sorted newest-first, shipping zero JavaScript.
15
+ Posts are plain Markdown files in `site/pages/posts/` — that folder is a typed
16
+ content collection, and this index is one `.wd` loop over it. No manifest, no
17
+ config, zero JavaScript; each post's frontmatter is the data.
16
18
 
17
- @loop /posts.json into post sort by post.date desc
19
+ @loop posts into post sort by post.date desc
18
20
  ::: post-card
19
21
  ### [{ post.title }]({ post.url })
20
- <p class="post-meta">{ post.date }</p>
22
+ ::: post-meta
23
+ { post.date }
24
+ :::
21
25
  { post.excerpt }
22
26
  :::
27
+ @empty
28
+ No posts yet — add a `.md` file to `site/pages/posts/`.
23
29
  @endloop
24
30
 
25
31
  </main>
@@ -0,0 +1,16 @@
1
+ ---
2
+ # Typed schema for the posts collection. Every .md file next to this file 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.
5
+ # Append "?" to a type to make a field optional. Page-shell flags a post sets
6
+ # (like transitions) are declared here too. Remove this file to turn
7
+ # validation off (it is opt-in).
8
+ title: string
9
+ date: date
10
+ description: string
11
+ excerpt: string?
12
+ transitions: boolean?
13
+ ---
14
+
15
+ The posts collection schema. Posts are plain `.md` files next to this file;
16
+ their frontmatter must match the rules above.
@@ -6,6 +6,8 @@ excerpt: I rewrote my blog as plain Markdown files — no React, no build ceremo
6
6
  transitions: true
7
7
  ---
8
8
 
9
+ [← All posts](/)
10
+
9
11
  # Hello, Darkmown
10
12
 
11
13
  This post is a plain `.md` file — strict CommonMark, **zero** framework
@@ -0,0 +1,59 @@
1
+ /* Colocated post styling — a .skin next to a .md attaches automatically.
2
+ Copy this file beside each new post to keep the reading design. */
3
+
4
+ tokens
5
+ ink #1d2530
6
+ paper #fbfaf7
7
+ panel #ffffff
8
+ line #e7e3da
9
+ muted #6b7785
10
+ accent #2f6df0
11
+
12
+ tokens dark
13
+ ink #e7ecf3
14
+ paper #0f141b
15
+ panel #161d27
16
+ line #28303c
17
+ muted #93a0b2
18
+ accent #5b8cff
19
+
20
+ html
21
+ bg $paper
22
+
23
+ page
24
+ max-width 680px
25
+ margin 0 auto
26
+ padding 2.5rem 1.5rem 4rem
27
+ font ui-serif, Georgia, Cambria, serif
28
+ color $ink
29
+ bg $paper
30
+ line-height 1.7
31
+ -webkit-font-smoothing antialiased
32
+
33
+ h1
34
+ font-size 2.1rem
35
+ line-height 1.2
36
+ margin 1rem 0 1.25rem
37
+
38
+ a
39
+ color $accent
40
+
41
+ code
42
+ font ui-monospace, SFMono-Regular, Menlo, monospace
43
+ font-size .88em
44
+ bg $panel
45
+ border 1px solid $line
46
+ padding .08rem .28rem
47
+ radius 5px
48
+
49
+ pre code
50
+ display block
51
+ padding .9rem 1.1rem
52
+ overflow-x auto
53
+
54
+ blockquote
55
+ margin 1rem 0
56
+ padding .5rem 1.2rem
57
+ border-left 3px solid $accent
58
+ bg $panel
59
+ color $muted
@@ -6,17 +6,22 @@ excerpt: Every post is a plain .md file; the index is one .wd loop. The date in
6
6
  transitions: true
7
7
  ---
8
8
 
9
+ [← All posts](/)
10
+
9
11
  # Markdown-native, by default
10
12
 
11
- To add a post, drop a Markdown file in `site/pages/posts/` and add a line to
12
- `site/_/posts.json`. The home page loops that manifest:
13
+ To add a post, drop a Markdown file in `site/pages/posts/`. That folder is a
14
+ typed content collection, and the home page loops it directly:
13
15
 
14
16
  ```wd
15
- @loop /posts.json into post sort by post.date desc
17
+ @loop posts into post sort by post.date desc
16
18
  ### [{ post.title }]({ post.url })
17
19
  @endloop
18
20
  ```
19
21
 
20
- No build config, no plugins, no content layer. The file *is* the page.
22
+ No manifest, no build config, no content layer. The file *is* the page, and its
23
+ frontmatter is the data — `site/pages/posts/_schema.wd` type-checks every
24
+ post's `title`, `date`, and `description` at build time, so a typo fails the
25
+ build with a file and line instead of shipping a broken list.
21
26
 
22
27
  [← Back to all posts](/)
@@ -0,0 +1,59 @@
1
+ /* Colocated post styling — a .skin next to a .md attaches automatically.
2
+ Copy this file beside each new post to keep the reading design. */
3
+
4
+ tokens
5
+ ink #1d2530
6
+ paper #fbfaf7
7
+ panel #ffffff
8
+ line #e7e3da
9
+ muted #6b7785
10
+ accent #2f6df0
11
+
12
+ tokens dark
13
+ ink #e7ecf3
14
+ paper #0f141b
15
+ panel #161d27
16
+ line #28303c
17
+ muted #93a0b2
18
+ accent #5b8cff
19
+
20
+ html
21
+ bg $paper
22
+
23
+ page
24
+ max-width 680px
25
+ margin 0 auto
26
+ padding 2.5rem 1.5rem 4rem
27
+ font ui-serif, Georgia, Cambria, serif
28
+ color $ink
29
+ bg $paper
30
+ line-height 1.7
31
+ -webkit-font-smoothing antialiased
32
+
33
+ h1
34
+ font-size 2.1rem
35
+ line-height 1.2
36
+ margin 1rem 0 1.25rem
37
+
38
+ a
39
+ color $accent
40
+
41
+ code
42
+ font ui-monospace, SFMono-Regular, Menlo, monospace
43
+ font-size .88em
44
+ bg $panel
45
+ border 1px solid $line
46
+ padding .08rem .28rem
47
+ radius 5px
48
+
49
+ pre code
50
+ display block
51
+ padding .9rem 1.1rem
52
+ overflow-x auto
53
+
54
+ blockquote
55
+ margin 1rem 0
56
+ padding .5rem 1.2rem
57
+ border-left 3px solid $accent
58
+ bg $panel
59
+ color $muted
@@ -1,3 +1,7 @@
1
+ ---
2
+ html: true
3
+ ---
4
+
1
5
  <nav>
2
6
  <strong>Dashboard</strong>
3
7
  <span class="links">
@@ -2,9 +2,13 @@ tokens
2
2
  ink #0f1722
3
3
  paper #0b1018
4
4
  panel #131c28
5
+ field-bg #0e1620
5
6
  line #243245
6
7
  muted #8aa0b8
7
8
  accent #4f9cff
9
+ accent-deep #6fb0ff
10
+ ring #4f9cff
11
+ radius 12px
8
12
 
9
13
  page
10
14
  margin 0
@@ -12,6 +16,7 @@ page
12
16
  color #e7eefc
13
17
  bg $paper
14
18
  line-height 1.55
19
+ -webkit-font-smoothing antialiased
15
20
 
16
21
  main
17
22
  max-width 880px
@@ -72,3 +77,82 @@ nav a
72
77
  color $muted
73
78
  font-size .85rem
74
79
  margin-top .5rem
80
+
81
+ /* Buttons & controls — refresh, filters, range pickers, etc. */
82
+ button
83
+ display inline-flex
84
+ align-items center
85
+ justify-content center
86
+ gap .5rem
87
+ bg $accent
88
+ color #07101d
89
+ border 1px solid $accent
90
+ padding .55rem 1.05rem
91
+ margin .4rem .5rem .4rem 0
92
+ radius 999px
93
+ font-size .92rem
94
+ font-weight 600
95
+ line-height 1.15
96
+ cursor pointer
97
+ appearance none
98
+ transition transform .14s cubic-bezier(.2,.7,.3,1), box-shadow .14s ease, background .14s ease, border-color .14s ease
99
+
100
+ button:hover
101
+ bg $accent-deep
102
+ border-color $accent-deep
103
+ transform translateY(-1px)
104
+ box-shadow 0 8px 20px -8px rgba(79,156,255,.6)
105
+
106
+ button:active
107
+ transform translateY(0)
108
+ box-shadow none
109
+
110
+ button:focus-visible
111
+ outline 2px solid $ring
112
+ outline-offset 2px
113
+
114
+ button:disabled
115
+ opacity .5
116
+ cursor not-allowed
117
+ transform none
118
+
119
+ button.ghost
120
+ bg transparent
121
+ color #e7eefc
122
+ border-color $line
123
+
124
+ button.ghost:hover
125
+ bg $panel
126
+ border-color $accent
127
+ box-shadow none
128
+
129
+ input, select, textarea
130
+ font inherit
131
+ font-size .95rem
132
+ color #e7eefc
133
+ bg $field-bg
134
+ border 1px solid $line
135
+ radius 10px
136
+ padding .55rem .8rem
137
+ margin .35rem 0
138
+ width 100%
139
+ max-width 24rem
140
+ appearance none
141
+ transition border-color .14s ease, box-shadow .14s ease
142
+
143
+ input::placeholder, textarea::placeholder
144
+ color $muted
145
+
146
+ input:hover, select:hover, textarea:hover
147
+ border-color #2f4763
148
+
149
+ input:focus-visible, select:focus-visible, textarea:focus-visible
150
+ outline none
151
+ border-color $accent
152
+ box-shadow 0 0 0 3px rgba(79,156,255,.22)
153
+
154
+ @media (prefers-reduced-motion: reduce)
155
+ button
156
+ transition none
157
+ button:hover
158
+ transform none
@@ -1,4 +1,5 @@
1
1
  ---
2
+ html: true
2
3
  title: Dashboard — built with Darkmown
3
4
  description: A live dashboard — stat cards fetched from a serverless function, on an otherwise static page.
4
5
  transitions: true
@@ -1,3 +1,7 @@
1
+ ---
2
+ html: true
3
+ ---
4
+
1
5
  <nav>
2
6
  <strong>Acme</strong>
3
7
  <span class="links">