@zvndev/darkmown 0.18.0 → 0.20.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/src/skin.js CHANGED
@@ -14,7 +14,8 @@ const aliases = new Map([
14
14
  * @returns {string}
15
15
  */
16
16
  function resolveProp(prop, rest) {
17
- if (prop === "font" && (/^[\d.]/.test(rest[0] || "") || rest.join(" ").includes("/"))) return "font";
17
+ if (prop === "font" && (/^[\d.]/.test(rest[0] || "") || rest.join(" ").includes("/")))
18
+ return "font";
18
19
  return aliases.get(prop) || prop;
19
20
  }
20
21
 
@@ -53,7 +54,16 @@ export function compileSkin(source) {
53
54
  /** @type {number | null} */
54
55
  let tokenIndent = null;
55
56
  /** @type {string[]} */
57
+ let tokenVars = [];
58
+ /** @type {string[]} */
56
59
  const rootVars = [];
60
+ /** @type {string[]} */
61
+ const darkVars = [];
62
+ // Manual-toggle variants: `tokens [data-theme=dark]` collects custom
63
+ // properties under an attribute-scoped `:root` so a `:store theme` toggle can
64
+ // flip them with no JS beyond writing the attribute.
65
+ /** @type {Map<string, string[]>} */
66
+ const attrVars = new Map();
57
67
 
58
68
  for (let i = 0; i < lines.length; i++) {
59
69
  const { indent, text } = lines[i];
@@ -61,14 +71,41 @@ export function compileSkin(source) {
61
71
  // the next line is indented deeper. Everything else is a declaration.
62
72
  const opensBlock = i + 1 < lines.length && lines[i + 1].indent > indent;
63
73
 
64
- if (text === "tokens" && opensBlock) {
74
+ // `tokens` opens a design-token block emitting `:root { --name: value }`.
75
+ // An optional modifier picks a variant: `tokens dark` collects into a
76
+ // `prefers-color-scheme: dark` override (OS-driven), while a bracketed
77
+ // `tokens [data-theme=dark]` scopes the override to a `:root` attribute for
78
+ // manual toggling. Both reuse the same `--name` properties.
79
+ // Capture the whole modifier (`.+`, not `\S+`) so a malformed bracketed
80
+ // modifier with internal spaces (`[data-theme=dark extra]`) is still routed
81
+ // to normalizeAttr's null fail-safe rather than slipping through as a bogus
82
+ // `tokens …` selector.
83
+ const tokenMatch = /^tokens(?:\s+(.+))?$/.exec(text);
84
+ if (tokenMatch && opensBlock) {
65
85
  tokenIndent = indent;
86
+ const modifier = tokenMatch[1];
87
+ if (!modifier) {
88
+ tokenVars = rootVars;
89
+ } else if (modifier === "dark") {
90
+ tokenVars = darkVars;
91
+ } else if (modifier.startsWith("[") && modifier.endsWith("]")) {
92
+ const attr = normalizeAttr(modifier);
93
+ if (attr) {
94
+ const selector = `:root${attr}`;
95
+ tokenVars = attrVars.get(selector) ?? [];
96
+ attrVars.set(selector, tokenVars);
97
+ } else {
98
+ tokenVars = rootVars; // malformed `[…]` modifier → fall back to default :root tokens
99
+ }
100
+ } else {
101
+ tokenVars = rootVars;
102
+ }
66
103
  continue;
67
104
  }
68
105
 
69
106
  if (tokenIndent !== null && indent > tokenIndent) {
70
107
  const [name, ...rest] = text.split(/\s+/);
71
- rootVars.push(` --${name}: ${rest.join(" ")};`);
108
+ tokenVars.push(` --${name}: ${rest.join(" ")};`);
72
109
  continue;
73
110
  }
74
111
 
@@ -96,10 +133,32 @@ export function compileSkin(source) {
96
133
  css.push(media ? `${media} { ${rule} }` : rule);
97
134
  }
98
135
 
136
+ // Prepend tokens ahead of selector rules. `unshift` reverses insertion order,
137
+ // so push the overrides first to land them after the `:root` defaults.
138
+ for (const [selector, vars] of attrVars) {
139
+ if (vars.length) css.unshift(`${selector} {\n${vars.join("\n")}\n}`);
140
+ }
141
+ if (darkVars.length) {
142
+ css.unshift(`@media (prefers-color-scheme: dark) {\n:root {\n${darkVars.join("\n")}\n}\n}`);
143
+ }
99
144
  if (rootVars.length) css.unshift(`:root {\n${rootVars.join("\n")}\n}`);
100
145
  return css.join("\n");
101
146
  }
102
147
 
148
+ /**
149
+ * Normalize a bracketed token modifier into a valid attribute selector,
150
+ * quoting the value so `[data-theme=dark]` becomes `[data-theme="dark"]`.
151
+ * Returns null for a malformed modifier (`[]`, `[a=b c]`, injection-shaped
152
+ * input) so the caller can fall back to default `:root` tokens instead of
153
+ * emitting a junk selector.
154
+ * @param {string} bracketed
155
+ * @returns {string | null}
156
+ */
157
+ function normalizeAttr(bracketed) {
158
+ const m = bracketed.match(/^\[\s*([^\]=\s]+)\s*=\s*"?([^\]"]*)"?\s*\]$/);
159
+ return m ? `[${m[1]}="${m[2]}"]` : null;
160
+ }
161
+
103
162
  /**
104
163
  * Combine a selector with its parent (handles `&` nesting and comma lists).
105
164
  * @param {string} text
package/src/statics.js CHANGED
@@ -1,6 +1,19 @@
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";
5
+
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>}
12
+ */
13
+ const HTML_SECURITY_HEADERS = {
14
+ ...BASE_SECURITY_HEADERS,
15
+ "Content-Security-Policy": REACTIVE_CSP
16
+ };
4
17
 
5
18
  /**
6
19
  * Serve a built file for a request URL out of `distRoot`, or a 404 page.
@@ -12,7 +25,7 @@ import path from "node:path";
12
25
  export function serve(distRoot, url, res) {
13
26
  const file = resolvePublicFile(distRoot, url || "/");
14
27
  if (!file || !fs.existsSync(file)) {
15
- res.writeHead(404, { "content-type": "text/html; charset=utf-8" });
28
+ res.writeHead(404, { "content-type": "text/html; charset=utf-8", ...HTML_SECURITY_HEADERS });
16
29
  const notFound = path.join(distRoot, "404.html");
17
30
  if (fs.existsSync(notFound)) {
18
31
  res.end(fs.readFileSync(notFound));
@@ -21,7 +34,11 @@ export function serve(distRoot, url, res) {
21
34
  res.end("<h1>Not found</h1><p>This route is hidden or has not been created.</p>");
22
35
  return;
23
36
  }
24
- res.writeHead(200, { "content-type": contentType(file) });
37
+ const type = contentType(file);
38
+ const headers = type.startsWith("text/html")
39
+ ? { "content-type": type, ...HTML_SECURITY_HEADERS }
40
+ : { "content-type": type };
41
+ res.writeHead(200, headers);
25
42
  fs.createReadStream(file).pipe(res);
26
43
  }
27
44
 
package/site/_/og.png DELETED
Binary file