@stacksjs/stx 0.2.121 → 0.2.123

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,20 @@
1
+ /**
2
+ * Transform every `@appearanceBootstrap({...})` directive in a template.
3
+ */
4
+ export declare function processAppearanceBootstrapDirective(template: string, context: Record<string, unknown>): string;
5
+ export declare interface AppearanceBootstrapValue {
6
+ key: string
7
+ attribute: string
8
+ allowed: string[]
9
+ default: string
10
+ }
11
+ export declare interface ColorModeBootstrapValue {
12
+ key: string
13
+ attribute: string
14
+ default: 'light' | 'dark' | 'system'
15
+ }
16
+ export declare interface AppearanceBootstrapOptions {
17
+ storageKey: string
18
+ appearance: AppearanceBootstrapValue
19
+ colorMode: ColorModeBootstrapValue
20
+ }
@@ -0,0 +1,92 @@
1
+ import { safeEvaluate } from "./safe-evaluator.js";
2
+ const DATA_ATTRIBUTE_PATTERN = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/, PROPERTY_PATTERN = /^[A-Za-z_$][\w$]*$/, COLOR_MODES = new Set(["light", "dark", "system"]);
3
+ function assertString(value, label) {
4
+ if (typeof value !== "string" || value.length === 0)
5
+ throw Error(`@appearanceBootstrap ${label} must be a non-empty string`);
6
+ }
7
+ function normalizeOptions(value) {
8
+ if (!value || typeof value !== "object" || Array.isArray(value))
9
+ throw Error("@appearanceBootstrap expects an options object");
10
+ const input = value, appearance = input.appearance, colorMode = input.colorMode;
11
+ assertString(input.storageKey, "storageKey");
12
+ if (!appearance || typeof appearance !== "object" || Array.isArray(appearance))
13
+ throw Error("@appearanceBootstrap appearance must be an object");
14
+ if (!colorMode || typeof colorMode !== "object" || Array.isArray(colorMode))
15
+ throw Error("@appearanceBootstrap colorMode must be an object");
16
+ assertString(appearance.key, "appearance.key");
17
+ assertString(appearance.attribute, "appearance.attribute");
18
+ assertString(appearance.default, "appearance.default");
19
+ assertString(colorMode.key, "colorMode.key");
20
+ assertString(colorMode.attribute, "colorMode.attribute");
21
+ assertString(colorMode.default, "colorMode.default");
22
+ if (!PROPERTY_PATTERN.test(appearance.key) || !PROPERTY_PATTERN.test(colorMode.key))
23
+ throw Error("@appearanceBootstrap preference keys must be JavaScript property names");
24
+ if (!DATA_ATTRIBUTE_PATTERN.test(appearance.attribute) || !DATA_ATTRIBUTE_PATTERN.test(colorMode.attribute))
25
+ throw Error("@appearanceBootstrap attributes must be lowercase data attribute names");
26
+ if (!Array.isArray(appearance.allowed) || appearance.allowed.length === 0 || appearance.allowed.some((item) => typeof item !== "string" || item.length === 0))
27
+ throw Error("@appearanceBootstrap appearance.allowed must contain one or more strings");
28
+ if (!appearance.allowed.includes(appearance.default))
29
+ throw Error("@appearanceBootstrap appearance.default must be in appearance.allowed");
30
+ if (!COLOR_MODES.has(colorMode.default))
31
+ throw Error("@appearanceBootstrap colorMode.default must be light, dark, or system");
32
+ return {
33
+ storageKey: input.storageKey,
34
+ appearance: {
35
+ key: appearance.key,
36
+ attribute: appearance.attribute,
37
+ allowed: [...new Set(appearance.allowed)],
38
+ default: appearance.default
39
+ },
40
+ colorMode: {
41
+ key: colorMode.key,
42
+ attribute: colorMode.attribute,
43
+ default: colorMode.default
44
+ }
45
+ };
46
+ }
47
+ function serializeForScript(value) {
48
+ return JSON.stringify(value).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
49
+ }
50
+ function generateBootstrap(options) {
51
+ return `<script data-stx-scoped data-stx-appearance-bootstrap>(function(){"use strict";const config=${serializeForScript(options)};const root=document.documentElement;let stored={};try{const raw=localStorage.getItem(config.storageKey);const parsed=raw?JSON.parse(raw):{};if(parsed&&typeof parsed==="object"&&!Array.isArray(parsed))stored=parsed}catch{}const selectedAppearance=config.appearance.allowed.includes(stored[config.appearance.key])?stored[config.appearance.key]:config.appearance.default;const selectedMode=["light","dark","system"].includes(stored[config.colorMode.key])?stored[config.colorMode.key]:config.colorMode.default;root.setAttribute("data-"+config.appearance.attribute,selectedAppearance);root.setAttribute("data-"+config.colorMode.attribute,selectedMode);let dark=selectedMode==="dark";if(selectedMode==="system"){try{dark=matchMedia("(prefers-color-scheme: dark)").matches}catch{dark=false}}root.classList.toggle("dark",dark);root.dataset.theme=dark?"dark":"light"}());</script>`;
52
+ }
53
+ function findClosingParenthesis(source, openIndex) {
54
+ let depth = 1, quote = null, escaped = !1;
55
+ for (let index = openIndex + 1;index < source.length; index++) {
56
+ const character = source[index];
57
+ if (escaped) {
58
+ escaped = !1;
59
+ continue;
60
+ }
61
+ if (quote) {
62
+ if (character === "\\")
63
+ escaped = !0;
64
+ else if (character === quote)
65
+ quote = null;
66
+ continue;
67
+ }
68
+ if (character === '"' || character === "'" || character === "`") {
69
+ quote = character;
70
+ continue;
71
+ }
72
+ if (character === "(")
73
+ depth++;
74
+ else if (character === ")" && --depth === 0)
75
+ return index;
76
+ }
77
+ return -1;
78
+ }
79
+ export function processAppearanceBootstrapDirective(template, context) {
80
+ let output = template;
81
+ const pattern = /@appearanceBootstrap\s*\(/g;
82
+ let match;
83
+ while ((match = pattern.exec(output)) !== null) {
84
+ const openIndex = match.index + match[0].length - 1, closeIndex = findClosingParenthesis(output, openIndex);
85
+ if (closeIndex === -1)
86
+ throw Error("@appearanceBootstrap has an unclosed options object");
87
+ const expression = output.slice(openIndex + 1, closeIndex).trim(), evaluated = safeEvaluate(`(${expression})`, context), replacement = generateBootstrap(normalizeOptions(evaluated));
88
+ output = output.slice(0, match.index) + replacement + output.slice(closeIndex + 1);
89
+ pattern.lastIndex = match.index + replacement.length;
90
+ }
91
+ return output;
92
+ }