hd-garden 0.5.10 → 0.5.11
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/app/styles/shell.css
CHANGED
|
@@ -65,7 +65,10 @@ html[data-mod="other"] .sh-search .kbd-other{display:inline}
|
|
|
65
65
|
.sh-icon{display:inline-grid;place-items:center;width:40px;height:40px;border:0;background:transparent;color:var(--soft);border-radius:var(--r);cursor:pointer;transition:background-color .15s ease,color .15s ease}
|
|
66
66
|
@media (hover:hover){.sh-icon:hover{background:var(--tint);color:var(--ink)}}
|
|
67
67
|
.sh-menu-btn,.sh-search-m{display:none}
|
|
68
|
-
|
|
68
|
+
.i-sun,html[data-theme="dark"] .i-moon{display:none}
|
|
69
|
+
html[data-theme="dark"] .i-sun{display:inline}
|
|
70
|
+
/* No JavaScript: the theme cannot be switched, the page follows the system (build/dark-fallback-postcss.mjs). */
|
|
71
|
+
@media (scripting:none){.sh-theme{display:none}}
|
|
69
72
|
.sh-menu{display:none}
|
|
70
73
|
|
|
71
74
|
/* ---------- page frame ---------- */
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// Dark fallback for pages without JavaScript.
|
|
2
|
+
//
|
|
3
|
+
// The theme script sets <html data-theme> before first paint. With scripting
|
|
4
|
+
// off nothing sets it, so the page would stay light on a dark system. Every
|
|
5
|
+
// rule written for `:root[data-theme="dark"]` or `html[data-theme="dark"]`
|
|
6
|
+
// gets a twin right after it, inside `@media (prefers-color-scheme: dark)`,
|
|
7
|
+
// that matches the root when it has no data-theme at all. The dark styles are
|
|
8
|
+
// written once; a theme the user picked (data-theme set) always wins, and
|
|
9
|
+
// the twin keeps the original specificity and source order.
|
|
10
|
+
//
|
|
11
|
+
// Any other use of data-theme that would leave a no-JS dark page wrong stops
|
|
12
|
+
// the build with an error naming the selector, instead of being skipped.
|
|
13
|
+
|
|
14
|
+
export const MEDIA = "(prefers-color-scheme: dark)";
|
|
15
|
+
const DARK_ROOT = /^(:root|html)\[data-theme=(["']?)dark\2\]/;
|
|
16
|
+
const ANY_THEME = /\[data-theme(?:([~|^$*]?=)\s*(["']?)([^"'\]]*)\2)?\s*\]/g;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Sort one selector (no commas):
|
|
20
|
+
* - "dark": starts with the dark root, gets a twin;
|
|
21
|
+
* - "other": no data-theme on the page root's dark side (light root, scoped
|
|
22
|
+
* components such as `.garden-ui-preview[data-theme="dark"]`), left alone;
|
|
23
|
+
* - throws for forms the twin cannot cover.
|
|
24
|
+
* `siblings` are the other selectors of the same rule.
|
|
25
|
+
*/
|
|
26
|
+
export function classify(selector, siblings = []) {
|
|
27
|
+
const value = selector.trim();
|
|
28
|
+
if (!value.includes("data-theme")) return "other";
|
|
29
|
+
const uses = [...value.matchAll(ANY_THEME)];
|
|
30
|
+
let dark = false;
|
|
31
|
+
for (const use of uses) {
|
|
32
|
+
const [whole, op, , theme] = use;
|
|
33
|
+
const before = value.slice(0, use.index);
|
|
34
|
+
const compound = before.match(/[^\s>+~(]*$/)[0];
|
|
35
|
+
if (op && op !== "=") throw new Error(`unsupported data-theme operator "${whole}" in "${value}"`);
|
|
36
|
+
if (!op) throw new Error(`data-theme without a value ("${whole}") in "${value}"`);
|
|
37
|
+
if (/:not\($/.test(before)) throw new Error(`negated theme "${whole}" in "${value}": write the dark case as a [data-theme="dark"] rule instead`);
|
|
38
|
+
if (theme !== "dark" && theme !== "light") throw new Error(`unknown theme "${theme}" in "${value}"`);
|
|
39
|
+
const isRoot = compound === ":root" || compound === "html";
|
|
40
|
+
if (isRoot) {
|
|
41
|
+
if (before !== compound) throw new Error(`root theme selector must start the selector: "${value}"`);
|
|
42
|
+
if (theme === "dark") dark = true;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (compound === "") {
|
|
46
|
+
// Bare `[data-theme="dark"] .x` also matches the page root. Allowed only
|
|
47
|
+
// when the same rule lists the root form, whose twin covers the page.
|
|
48
|
+
const rest = value.slice(use.index + whole.length);
|
|
49
|
+
const covered = siblings.some((other) => other !== value && DARK_ROOT.test(other.trim()) && other.trim().replace(DARK_ROOT, "") === rest);
|
|
50
|
+
if (theme === "dark" && !covered) throw new Error(`bare "${whole}" in "${value}" also matches the page root; list ":root${whole}${rest}" in the same rule`);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (/^[.#]/.test(compound)) continue; // scoped component with its own data-theme
|
|
54
|
+
throw new Error(`unsupported data-theme selector "${compound}${whole}" in "${value}"`);
|
|
55
|
+
}
|
|
56
|
+
return dark ? "dark" : "other";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** `html[data-theme="dark"] .x` → `html:not([data-theme]) .x`. */
|
|
60
|
+
export function fallbackSelector(selector) {
|
|
61
|
+
const value = selector.trim();
|
|
62
|
+
return DARK_ROOT.test(value) ? value.replace(DARK_ROOT, (_, root) => `${root}:not([data-theme])`) : null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export default function darkFallback() {
|
|
66
|
+
return {
|
|
67
|
+
postcssPlugin: "garden-dark-fallback",
|
|
68
|
+
OnceExit(root, { AtRule }) {
|
|
69
|
+
root.walkRules((rule) => {
|
|
70
|
+
if (rule.parent?.type === "atrule" && rule.parent.name === "media" && rule.parent.params === MEDIA) return;
|
|
71
|
+
if (!rule.selector.includes("data-theme")) return;
|
|
72
|
+
const parts = rule.selectors;
|
|
73
|
+
const twins = [];
|
|
74
|
+
for (const part of parts) {
|
|
75
|
+
let kind;
|
|
76
|
+
try {
|
|
77
|
+
kind = classify(part, parts);
|
|
78
|
+
} catch (error) {
|
|
79
|
+
throw rule.error(`garden dark fallback: ${error.message}`, { word: part.trim() });
|
|
80
|
+
}
|
|
81
|
+
if (kind === "dark") twins.push(fallbackSelector(part));
|
|
82
|
+
}
|
|
83
|
+
if (!twins.length) return;
|
|
84
|
+
const next = rule.next();
|
|
85
|
+
if (next?.type === "atrule" && next.name === "media" && next.params === MEDIA) return; // already has its twin
|
|
86
|
+
const media = new AtRule({ name: "media", params: MEDIA });
|
|
87
|
+
media.append(rule.clone({ selectors: twins }));
|
|
88
|
+
rule.after(media);
|
|
89
|
+
});
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
darkFallback.postcss = true;
|
package/package.json
CHANGED
package/postcss.config.mjs
CHANGED