@burojs/ui-shadcn 0.3.0 → 0.4.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/README.md +4 -2
- package/dist/index.cjs +15044 -16998
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3488 -3105
- package/dist/index.d.ts +3488 -3105
- package/dist/index.js +14965 -16860
- package/dist/index.js.map +1 -1
- package/dist/layout/theme-storage-key.cjs +5 -10
- package/dist/layout/theme-storage-key.cjs.map +1 -1
- package/dist/layout/theme-storage-key.d.cts +2 -23
- package/dist/layout/theme-storage-key.d.ts +2 -23
- package/dist/layout/theme-storage-key.js +5 -2
- package/dist/layout/theme-storage-key.js.map +1 -1
- package/dist/markdown/index.cjs +424 -511
- package/dist/markdown/index.cjs.map +1 -1
- package/dist/markdown/index.d.cts +1072 -34
- package/dist/markdown/index.d.ts +1072 -34
- package/dist/markdown/index.js +411 -459
- package/dist/markdown/index.js.map +1 -1
- package/dist/rich-text-IDFvyvRE.js +454 -0
- package/dist/rich-text-IDFvyvRE.js.map +1 -0
- package/dist/rich-text-lndbYqzR.cjs +457 -0
- package/dist/rich-text-lndbYqzR.cjs.map +1 -0
- package/dist/styles.css +14 -8
- package/dist/theme/theme-css.cjs +95 -96
- package/dist/theme/theme-css.cjs.map +1 -1
- package/dist/theme/theme-css.d.cts +10 -8
- package/dist/theme/theme-css.d.ts +10 -8
- package/dist/theme/theme-css.js +94 -94
- package/dist/theme/theme-css.js.map +1 -1
- package/dist/theme/theme-storage.cjs +34 -39
- package/dist/theme/theme-storage.cjs.map +1 -1
- package/dist/theme/theme-storage.d.cts +3 -1
- package/dist/theme/theme-storage.d.ts +3 -1
- package/dist/theme/theme-storage.js +33 -37
- package/dist/theme/theme-storage.js.map +1 -1
- package/dist/theme-storage-key-pYlp4aDS.d.cts +31 -0
- package/dist/theme-storage-key-pYlp4aDS.d.ts +31 -0
- package/dist/use-theme-BwPWinB2.cjs +246 -0
- package/dist/use-theme-BwPWinB2.cjs.map +1 -0
- package/dist/use-theme-IHmtrhEt.js +170 -0
- package/dist/use-theme-IHmtrhEt.js.map +1 -0
- package/package.json +10 -11
- package/dist/chunk-ABUQPOOE.js +0 -11
- package/dist/chunk-ABUQPOOE.js.map +0 -1
- package/dist/chunk-DBGGWBFG.cjs +0 -199
- package/dist/chunk-DBGGWBFG.cjs.map +0 -1
- package/dist/chunk-FMX4GLCQ.js +0 -6
- package/dist/chunk-FMX4GLCQ.js.map +0 -1
- package/dist/chunk-UC57Z243.js +0 -143
- package/dist/chunk-UC57Z243.js.map +0 -1
- package/dist/chunk-UPGPO75B.js +0 -164
- package/dist/chunk-UPGPO75B.js.map +0 -1
- package/dist/chunk-VXJAXJ3U.cjs +0 -8
- package/dist/chunk-VXJAXJ3U.cjs.map +0 -1
- package/dist/chunk-W7AEYE7A.cjs +0 -13
- package/dist/chunk-W7AEYE7A.cjs.map +0 -1
- package/dist/chunk-YF5IW525.cjs +0 -155
- package/dist/chunk-YF5IW525.cjs.map +0 -1
- package/dist/rich-text-6V7WZ3WV.cjs +0 -518
- package/dist/rich-text-6V7WZ3WV.cjs.map +0 -1
- package/dist/rich-text-N2FZEJJV.js +0 -491
- package/dist/rich-text-N2FZEJJV.js.map +0 -1
package/dist/theme/theme-css.cjs
CHANGED
|
@@ -1,105 +1,104 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
// src/theme/theme-css.ts
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region src/theme/theme-css.ts
|
|
4
3
|
function parseTopLevelBlocks(src) {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
4
|
+
const n = src.length;
|
|
5
|
+
const blocks = [];
|
|
6
|
+
let i = 0;
|
|
7
|
+
const skipComment = (pos) => {
|
|
8
|
+
const end = src.indexOf("*/", pos + 2);
|
|
9
|
+
return end === -1 ? n : end + 2;
|
|
10
|
+
};
|
|
11
|
+
const skipString = (pos) => {
|
|
12
|
+
const quote = src[pos];
|
|
13
|
+
let j = pos + 1;
|
|
14
|
+
while (j < n && src[j] !== quote) {
|
|
15
|
+
if (src[j] === "\\") j++;
|
|
16
|
+
j++;
|
|
17
|
+
}
|
|
18
|
+
return j < n ? j + 1 : n;
|
|
19
|
+
};
|
|
20
|
+
while (i < n) {
|
|
21
|
+
while (i < n) {
|
|
22
|
+
const ch = src[i];
|
|
23
|
+
if (ch === " " || ch === " " || ch === "\n" || ch === "\r") {
|
|
24
|
+
i++;
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (src.startsWith("/*", i)) {
|
|
28
|
+
i = skipComment(i);
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
if (i >= n) break;
|
|
34
|
+
const statementStart = i;
|
|
35
|
+
let head = "";
|
|
36
|
+
while (i < n && src[i] !== "{" && src[i] !== ";") {
|
|
37
|
+
if (src.startsWith("/*", i)) {
|
|
38
|
+
i = skipComment(i);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (src[i] === "\"" || src[i] === "'") {
|
|
42
|
+
const strStart = i;
|
|
43
|
+
i = skipString(i);
|
|
44
|
+
head += src.slice(strStart, i);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
head += src[i];
|
|
48
|
+
i++;
|
|
49
|
+
}
|
|
50
|
+
if (i >= n) break;
|
|
51
|
+
if (src[i] === ";") {
|
|
52
|
+
i++;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
let depth = 1;
|
|
56
|
+
let j = i + 1;
|
|
57
|
+
while (j < n && depth > 0) {
|
|
58
|
+
if (src.startsWith("/*", j)) {
|
|
59
|
+
j = skipComment(j);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (src[j] === "\"" || src[j] === "'") {
|
|
63
|
+
j = skipString(j);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (src[j] === "{") depth++;
|
|
67
|
+
else if (src[j] === "}") depth--;
|
|
68
|
+
j++;
|
|
69
|
+
}
|
|
70
|
+
blocks.push({
|
|
71
|
+
selector: head.trim(),
|
|
72
|
+
raw: src.slice(statementStart, j)
|
|
73
|
+
});
|
|
74
|
+
i = j;
|
|
75
|
+
}
|
|
76
|
+
return blocks;
|
|
75
77
|
}
|
|
76
78
|
function stripThemeCss(raw) {
|
|
77
|
-
|
|
78
|
-
const blocks = parseTopLevelBlocks(normalized);
|
|
79
|
-
const kept = blocks.filter((b) => b.selector === ":root" || b.selector === ".dark");
|
|
80
|
-
return kept.map((b) => b.raw).join("\n\n");
|
|
79
|
+
return parseTopLevelBlocks(raw.replace(/\r\n/g, "\n")).filter((b) => b.selector === ":root" || b.selector === ".dark").map((b) => b.raw).join("\n\n");
|
|
81
80
|
}
|
|
82
81
|
function validateThemeCss(css) {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
82
|
+
const normalized = css.replace(/\r\n/g, "\n");
|
|
83
|
+
if (normalized.trim().length === 0) return {
|
|
84
|
+
ok: false,
|
|
85
|
+
reason: "Theme CSS is empty."
|
|
86
|
+
};
|
|
87
|
+
const blocks = parseTopLevelBlocks(normalized);
|
|
88
|
+
const hasDark = blocks.some((b) => b.selector === ".dark");
|
|
89
|
+
const hasRoot = blocks.some((b) => b.selector === ":root");
|
|
90
|
+
if (!hasDark) return {
|
|
91
|
+
ok: false,
|
|
92
|
+
reason: "Theme has no \".dark\" block. Without it, the theme's \":root\" values override the app's \".dark\" styles at equal CSS specificity, and dark mode silently stops working."
|
|
93
|
+
};
|
|
94
|
+
if (!hasRoot) return {
|
|
95
|
+
ok: false,
|
|
96
|
+
reason: "Theme has no \":root\" block — light-mode values are missing."
|
|
97
|
+
};
|
|
98
|
+
return { ok: true };
|
|
100
99
|
}
|
|
101
|
-
|
|
100
|
+
//#endregion
|
|
102
101
|
exports.stripThemeCss = stripThemeCss;
|
|
103
102
|
exports.validateThemeCss = validateThemeCss;
|
|
104
|
-
|
|
103
|
+
|
|
105
104
|
//# sourceMappingURL=theme-css.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/theme/theme-css.ts"],"names":[],"mappings":"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","file":"theme-css.cjs","sourcesContent":["/**\n * Pure functions for handling a pasted tweakcn theme.\n *\n * tweakcn's \"Copy code\" button emits six constructs. Only two of them mean\n * anything once dropped into a browser at runtime (not build time, since\n * there is no Tailwind build step for pasted CSS):\n *\n * @import \"tailwindcss\"; -> 404s if kept (no such route at runtime)\n * @custom-variant dark (...); -> meaningless outside a Tailwind build\n * :root { ... } -> survives, sets light-mode custom properties\n * .dark { ... } -> survives, sets dark-mode custom properties\n * @theme inline { ... } -> meaningless outside a Tailwind build\n * @layer base { ...; @apply ...; } -> @apply is not real CSS, dead weight\n *\n * `stripThemeCss` throws away everything except the `:root` and `.dark`\n * blocks. `validateThemeCss` catches the one failure mode that is otherwise\n * invisible: a theme with no `.dark` block. `.dark` and `:root` share equal\n * specificity, so injecting a `:root`-only theme *after* the app's own\n * stylesheet makes its `:root` win over `html.dark`'s rules too — dark mode\n * silently stops working, with no error anywhere.\n */\n\nexport type ThemeRecord = { id: string; name: string; css: string };\n\nexport type ThemeValidationResult = { ok: true } | { ok: false; reason: string };\n\ninterface TopLevelBlock {\n /** The selector/at-rule head, trimmed (e.g. \":root\", \".dark\", \"@theme inline\"). */\n selector: string;\n /** The full source slice for this block, selector through closing brace. */\n raw: string;\n}\n\n/**\n * Walks the source once, splitting it into top-level statements. Any\n * statement terminated by `;` (e.g. `@import \"tailwindcss\";`,\n * `@custom-variant dark (&:is(.dark *));`) is dropped outright — it never\n * produces a block. Any statement with a `{ ... }` body is returned as a\n * block, with brace depth tracked through the body so a nested block (e.g.\n * `@layer base { * { ... } body { ... } }`) is consumed as a single unit\n * instead of stopping at its first inner `}`.\n *\n * Comments and quoted strings are skipped transparently everywhere braces\n * are counted, so a brace inside `/* ... *\\/` or inside a string value never\n * perturbs the depth count.\n *\n * Unclosed input (an opened `{`, `/*`, or quote that never finds its\n * closer) is NOT dropped: the depth/comment/string scan simply runs off\n * the end of the string, and the block gets pushed with its `raw` spanning\n * from the opening selector all the way to EOF — swallowing whatever\n * well-formed blocks would otherwise have followed it. Concretely: an\n * unclosed `:root {` earlier in the source will absorb a perfectly valid\n * `.dark { ... }` that comes after it, so `.dark` never becomes its own\n * top-level block. This is a deliberately-left gap, not a bug: the\n * consequence is caught downstream by `validateThemeCss`'s \"no .dark\"\n * check, so truncated/corrupt input fails closed rather than corrupting\n * silently. The one sharp edge is the *reason string* it fails with — a\n * clipboard read that came back truncated will be reported as \"no .dark\n * block\", not \"your paste was cut off\", which is worth remembering when\n * debugging a fetch step that feeds this function.\n */\nfunction parseTopLevelBlocks(src: string): TopLevelBlock[] {\n const n = src.length;\n const blocks: TopLevelBlock[] = [];\n let i = 0;\n\n const skipComment = (pos: number): number => {\n const end = src.indexOf('*/', pos + 2);\n return end === -1 ? n : end + 2;\n };\n\n const skipString = (pos: number): number => {\n const quote = src[pos];\n let j = pos + 1;\n while (j < n && src[j] !== quote) {\n if (src[j] === '\\\\') j++;\n j++;\n }\n return j < n ? j + 1 : n;\n };\n\n while (i < n) {\n // Skip whitespace and comments between statements.\n while (i < n) {\n const ch = src[i];\n if (ch === ' ' || ch === '\\t' || ch === '\\n' || ch === '\\r') {\n i++;\n continue;\n }\n if (src.startsWith('/*', i)) {\n i = skipComment(i);\n continue;\n }\n break;\n }\n if (i >= n) break;\n\n const statementStart = i;\n let head = '';\n\n // Scan the head (selector / at-rule prelude) up to a top-level `{` or `;`.\n while (i < n && src[i] !== '{' && src[i] !== ';') {\n if (src.startsWith('/*', i)) {\n i = skipComment(i);\n continue;\n }\n if (src[i] === '\"' || src[i] === \"'\") {\n const strStart = i;\n i = skipString(i);\n head += src.slice(strStart, i);\n continue;\n }\n head += src[i];\n i++;\n }\n\n // Ran off the end while scanning the head itself (no `{` or `;` ever\n // showed up) — this fragment genuinely IS dropped, since there is no\n // `{` to even start a block from. Contrast with an opened-but-unclosed\n // `{ ... }` body below, which is NOT dropped — see the block comment\n // on this function.\n if (i >= n) break;\n\n if (src[i] === ';') {\n i++; // e.g. `@import \"tailwindcss\";` — drop, no block produced.\n continue;\n }\n\n // src[i] === '{' — consume the whole block, tracking nested brace depth.\n let depth = 1;\n let j = i + 1;\n while (j < n && depth > 0) {\n if (src.startsWith('/*', j)) {\n j = skipComment(j);\n continue;\n }\n if (src[j] === '\"' || src[j] === \"'\") {\n j = skipString(j);\n continue;\n }\n if (src[j] === '{') depth++;\n else if (src[j] === '}') depth--;\n j++;\n }\n\n // Pushed unconditionally — if `depth` never reached 0 the loop above\n // only stopped because `j` hit `n` (EOF). That's intentional: see the\n // \"Unclosed input\" paragraph on this function's doc comment.\n blocks.push({ selector: head.trim(), raw: src.slice(statementStart, j) });\n i = j;\n }\n\n return blocks;\n}\n\n/**\n * Reduces a pasted tweakcn theme to just its `:root { ... }` and\n * `.dark { ... }` blocks, in source order. Everything else (imports,\n * `@custom-variant`, `@theme inline`, `@layer base`) is dropped. Idempotent:\n * running it again on its own output is a no-op.\n */\nexport function stripThemeCss(raw: string): string {\n const normalized = raw.replace(/\\r\\n/g, '\\n');\n const blocks = parseTopLevelBlocks(normalized);\n const kept = blocks.filter((b) => b.selector === ':root' || b.selector === '.dark');\n return kept.map((b) => b.raw).join('\\n\\n');\n}\n\n/**\n * Validates that a (normally already-stripped) theme is safe to inject.\n * The load-bearing check is `.dark`: without it, a theme's `:root` values\n * override the app's `.dark` rules at equal specificity, and dark mode\n * breaks silently. Also rejects empty input and (defensively) a theme with\n * no `:root` block at all.\n */\nexport function validateThemeCss(css: string): ThemeValidationResult {\n const normalized = css.replace(/\\r\\n/g, '\\n');\n if (normalized.trim().length === 0) {\n return { ok: false, reason: 'Theme CSS is empty.' };\n }\n\n const blocks = parseTopLevelBlocks(normalized);\n const hasDark = blocks.some((b) => b.selector === '.dark');\n const hasRoot = blocks.some((b) => b.selector === ':root');\n\n if (!hasDark) {\n return {\n ok: false,\n reason:\n 'Theme has no \".dark\" block. Without it, the theme\\'s \":root\" values ' +\n 'override the app\\'s \".dark\" styles at equal CSS specificity, and dark ' +\n 'mode silently stops working.',\n };\n }\n\n if (!hasRoot) {\n return { ok: false, reason: 'Theme has no \":root\" block — light-mode values are missing.' };\n }\n\n return { ok: true };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"theme-css.cjs","names":[],"sources":["../../src/theme/theme-css.ts"],"sourcesContent":["/**\n * Pure functions for handling a pasted tweakcn theme.\n *\n * tweakcn's \"Copy code\" button emits six constructs. Only two of them mean\n * anything once dropped into a browser at runtime (not build time, since\n * there is no Tailwind build step for pasted CSS):\n *\n * @import \"tailwindcss\"; -> 404s if kept (no such route at runtime)\n * @custom-variant dark (...); -> meaningless outside a Tailwind build\n * :root { ... } -> survives, sets light-mode custom properties\n * .dark { ... } -> survives, sets dark-mode custom properties\n * @theme inline { ... } -> meaningless outside a Tailwind build\n * @layer base { ...; @apply ...; } -> @apply is not real CSS, dead weight\n *\n * `stripThemeCss` throws away everything except the `:root` and `.dark`\n * blocks. `validateThemeCss` catches the one failure mode that is otherwise\n * invisible: a theme with no `.dark` block. `.dark` and `:root` share equal\n * specificity, so injecting a `:root`-only theme *after* the app's own\n * stylesheet makes its `:root` win over `html.dark`'s rules too — dark mode\n * silently stops working, with no error anywhere.\n */\n\nexport type ThemeRecord = { id: string; name: string; css: string };\n\nexport type ThemeValidationResult = { ok: true } | { ok: false; reason: string };\n\ninterface TopLevelBlock {\n /** The selector/at-rule head, trimmed (e.g. \":root\", \".dark\", \"@theme inline\"). */\n selector: string;\n /** The full source slice for this block, selector through closing brace. */\n raw: string;\n}\n\n/**\n * Walks the source once, splitting it into top-level statements. Any\n * statement terminated by `;` (e.g. `@import \"tailwindcss\";`,\n * `@custom-variant dark (&:is(.dark *));`) is dropped outright — it never\n * produces a block. Any statement with a `{ ... }` body is returned as a\n * block, with brace depth tracked through the body so a nested block (e.g.\n * `@layer base { * { ... } body { ... } }`) is consumed as a single unit\n * instead of stopping at its first inner `}`.\n *\n * Comments and quoted strings are skipped transparently everywhere braces\n * are counted, so a brace inside `/* ... *\\/` or inside a string value never\n * perturbs the depth count.\n *\n * Unclosed input (an opened `{`, `/*`, or quote that never finds its\n * closer) is NOT dropped: the depth/comment/string scan simply runs off\n * the end of the string, and the block gets pushed with its `raw` spanning\n * from the opening selector all the way to EOF — swallowing whatever\n * well-formed blocks would otherwise have followed it. Concretely: an\n * unclosed `:root {` earlier in the source will absorb a perfectly valid\n * `.dark { ... }` that comes after it, so `.dark` never becomes its own\n * top-level block. This is a deliberately-left gap, not a bug: the\n * consequence is caught downstream by `validateThemeCss`'s \"no .dark\"\n * check, so truncated/corrupt input fails closed rather than corrupting\n * silently. The one sharp edge is the *reason string* it fails with — a\n * clipboard read that came back truncated will be reported as \"no .dark\n * block\", not \"your paste was cut off\", which is worth remembering when\n * debugging a fetch step that feeds this function.\n */\nfunction parseTopLevelBlocks(src: string): TopLevelBlock[] {\n const n = src.length;\n const blocks: TopLevelBlock[] = [];\n let i = 0;\n\n const skipComment = (pos: number): number => {\n const end = src.indexOf('*/', pos + 2);\n return end === -1 ? n : end + 2;\n };\n\n const skipString = (pos: number): number => {\n const quote = src[pos];\n let j = pos + 1;\n while (j < n && src[j] !== quote) {\n if (src[j] === '\\\\') j++;\n j++;\n }\n return j < n ? j + 1 : n;\n };\n\n while (i < n) {\n // Skip whitespace and comments between statements.\n while (i < n) {\n const ch = src[i];\n if (ch === ' ' || ch === '\\t' || ch === '\\n' || ch === '\\r') {\n i++;\n continue;\n }\n if (src.startsWith('/*', i)) {\n i = skipComment(i);\n continue;\n }\n break;\n }\n if (i >= n) break;\n\n const statementStart = i;\n let head = '';\n\n // Scan the head (selector / at-rule prelude) up to a top-level `{` or `;`.\n while (i < n && src[i] !== '{' && src[i] !== ';') {\n if (src.startsWith('/*', i)) {\n i = skipComment(i);\n continue;\n }\n if (src[i] === '\"' || src[i] === \"'\") {\n const strStart = i;\n i = skipString(i);\n head += src.slice(strStart, i);\n continue;\n }\n head += src[i];\n i++;\n }\n\n // Ran off the end while scanning the head itself (no `{` or `;` ever\n // showed up) — this fragment genuinely IS dropped, since there is no\n // `{` to even start a block from. Contrast with an opened-but-unclosed\n // `{ ... }` body below, which is NOT dropped — see the block comment\n // on this function.\n if (i >= n) break;\n\n if (src[i] === ';') {\n i++; // e.g. `@import \"tailwindcss\";` — drop, no block produced.\n continue;\n }\n\n // src[i] === '{' — consume the whole block, tracking nested brace depth.\n let depth = 1;\n let j = i + 1;\n while (j < n && depth > 0) {\n if (src.startsWith('/*', j)) {\n j = skipComment(j);\n continue;\n }\n if (src[j] === '\"' || src[j] === \"'\") {\n j = skipString(j);\n continue;\n }\n if (src[j] === '{') depth++;\n else if (src[j] === '}') depth--;\n j++;\n }\n\n // Pushed unconditionally — if `depth` never reached 0 the loop above\n // only stopped because `j` hit `n` (EOF). That's intentional: see the\n // \"Unclosed input\" paragraph on this function's doc comment.\n blocks.push({ selector: head.trim(), raw: src.slice(statementStart, j) });\n i = j;\n }\n\n return blocks;\n}\n\n/**\n * Reduces a pasted tweakcn theme to just its `:root { ... }` and\n * `.dark { ... }` blocks, in source order. Everything else (imports,\n * `@custom-variant`, `@theme inline`, `@layer base`) is dropped. Idempotent:\n * running it again on its own output is a no-op.\n */\nexport function stripThemeCss(raw: string): string {\n const normalized = raw.replace(/\\r\\n/g, '\\n');\n const blocks = parseTopLevelBlocks(normalized);\n const kept = blocks.filter((b) => b.selector === ':root' || b.selector === '.dark');\n return kept.map((b) => b.raw).join('\\n\\n');\n}\n\n/**\n * Validates that a (normally already-stripped) theme is safe to inject.\n * The load-bearing check is `.dark`: without it, a theme's `:root` values\n * override the app's `.dark` rules at equal specificity, and dark mode\n * breaks silently. Also rejects empty input and (defensively) a theme with\n * no `:root` block at all.\n */\nexport function validateThemeCss(css: string): ThemeValidationResult {\n const normalized = css.replace(/\\r\\n/g, '\\n');\n if (normalized.trim().length === 0) {\n return { ok: false, reason: 'Theme CSS is empty.' };\n }\n\n const blocks = parseTopLevelBlocks(normalized);\n const hasDark = blocks.some((b) => b.selector === '.dark');\n const hasRoot = blocks.some((b) => b.selector === ':root');\n\n if (!hasDark) {\n return {\n ok: false,\n reason:\n 'Theme has no \".dark\" block. Without it, the theme\\'s \":root\" values ' +\n 'override the app\\'s \".dark\" styles at equal CSS specificity, and dark ' +\n 'mode silently stops working.',\n };\n }\n\n if (!hasRoot) {\n return { ok: false, reason: 'Theme has no \":root\" block — light-mode values are missing.' };\n }\n\n return { ok: true };\n}\n"],"mappings":";;AA6DA,SAAS,oBAAoB,KAA8B;CACzD,MAAM,IAAI,IAAI;CACd,MAAM,SAA0B,CAAC;CACjC,IAAI,IAAI;CAER,MAAM,eAAe,QAAwB;EAC3C,MAAM,MAAM,IAAI,QAAQ,MAAM,MAAM,CAAC;EACrC,OAAO,QAAQ,KAAK,IAAI,MAAM;CAChC;CAEA,MAAM,cAAc,QAAwB;EAC1C,MAAM,QAAQ,IAAI;EAClB,IAAI,IAAI,MAAM;EACd,OAAO,IAAI,KAAK,IAAI,OAAO,OAAO;GAChC,IAAI,IAAI,OAAO,MAAM;GACrB;EACF;EACA,OAAO,IAAI,IAAI,IAAI,IAAI;CACzB;CAEA,OAAO,IAAI,GAAG;EAEZ,OAAO,IAAI,GAAG;GACZ,MAAM,KAAK,IAAI;GACf,IAAI,OAAO,OAAO,OAAO,OAAQ,OAAO,QAAQ,OAAO,MAAM;IAC3D;IACA;GACF;GACA,IAAI,IAAI,WAAW,MAAM,CAAC,GAAG;IAC3B,IAAI,YAAY,CAAC;IACjB;GACF;GACA;EACF;EACA,IAAI,KAAK,GAAG;EAEZ,MAAM,iBAAiB;EACvB,IAAI,OAAO;EAGX,OAAO,IAAI,KAAK,IAAI,OAAO,OAAO,IAAI,OAAO,KAAK;GAChD,IAAI,IAAI,WAAW,MAAM,CAAC,GAAG;IAC3B,IAAI,YAAY,CAAC;IACjB;GACF;GACA,IAAI,IAAI,OAAO,QAAO,IAAI,OAAO,KAAK;IACpC,MAAM,WAAW;IACjB,IAAI,WAAW,CAAC;IAChB,QAAQ,IAAI,MAAM,UAAU,CAAC;IAC7B;GACF;GACA,QAAQ,IAAI;GACZ;EACF;EAOA,IAAI,KAAK,GAAG;EAEZ,IAAI,IAAI,OAAO,KAAK;GAClB;GACA;EACF;EAGA,IAAI,QAAQ;EACZ,IAAI,IAAI,IAAI;EACZ,OAAO,IAAI,KAAK,QAAQ,GAAG;GACzB,IAAI,IAAI,WAAW,MAAM,CAAC,GAAG;IAC3B,IAAI,YAAY,CAAC;IACjB;GACF;GACA,IAAI,IAAI,OAAO,QAAO,IAAI,OAAO,KAAK;IACpC,IAAI,WAAW,CAAC;IAChB;GACF;GACA,IAAI,IAAI,OAAO,KAAK;QACf,IAAI,IAAI,OAAO,KAAK;GACzB;EACF;EAKA,OAAO,KAAK;GAAE,UAAU,KAAK,KAAK;GAAG,KAAK,IAAI,MAAM,gBAAgB,CAAC;EAAE,CAAC;EACxE,IAAI;CACN;CAEA,OAAO;AACT;AAQA,SAAgB,cAAc,KAAqB;CAIjD,OAFe,oBADI,IAAI,QAAQ,SAAS,IACI,CAC1B,CAAC,CAAC,QAAQ,MAAM,EAAE,aAAa,WAAW,EAAE,aAAa,OACjE,CAAC,CAAC,KAAK,MAAM,EAAE,GAAG,CAAC,CAAC,KAAK,MAAM;AAC3C;AASA,SAAgB,iBAAiB,KAAoC;CACnE,MAAM,aAAa,IAAI,QAAQ,SAAS,IAAI;CAC5C,IAAI,WAAW,KAAK,CAAC,CAAC,WAAW,GAC/B,OAAO;EAAE,IAAI;EAAO,QAAQ;CAAsB;CAGpD,MAAM,SAAS,oBAAoB,UAAU;CAC7C,MAAM,UAAU,OAAO,MAAM,MAAM,EAAE,aAAa,OAAO;CACzD,MAAM,UAAU,OAAO,MAAM,MAAM,EAAE,aAAa,OAAO;CAEzD,IAAI,CAAC,SACH,OAAO;EACL,IAAI;EACJ,QACE;CAGJ;CAGF,IAAI,CAAC,SACH,OAAO;EAAE,IAAI;EAAO,QAAQ;CAA8D;CAG5F,OAAO,EAAE,IAAI,KAAK;AACpB"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
//#region src/theme/theme-css.d.ts
|
|
1
2
|
/**
|
|
2
3
|
* Pure functions for handling a pasted tweakcn theme.
|
|
3
4
|
*
|
|
@@ -20,15 +21,15 @@
|
|
|
20
21
|
* silently stops working, with no error anywhere.
|
|
21
22
|
*/
|
|
22
23
|
type ThemeRecord = {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
id: string;
|
|
25
|
+
name: string;
|
|
26
|
+
css: string;
|
|
26
27
|
};
|
|
27
28
|
type ThemeValidationResult = {
|
|
28
|
-
|
|
29
|
+
ok: true;
|
|
29
30
|
} | {
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
ok: false;
|
|
32
|
+
reason: string;
|
|
32
33
|
};
|
|
33
34
|
/**
|
|
34
35
|
* Reduces a pasted tweakcn theme to just its `:root { ... }` and
|
|
@@ -45,5 +46,6 @@ declare function stripThemeCss(raw: string): string;
|
|
|
45
46
|
* no `:root` block at all.
|
|
46
47
|
*/
|
|
47
48
|
declare function validateThemeCss(css: string): ThemeValidationResult;
|
|
48
|
-
|
|
49
|
-
export {
|
|
49
|
+
//#endregion
|
|
50
|
+
export { ThemeRecord, ThemeValidationResult, stripThemeCss, validateThemeCss };
|
|
51
|
+
//# sourceMappingURL=theme-css.d.cts.map
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
//#region src/theme/theme-css.d.ts
|
|
1
2
|
/**
|
|
2
3
|
* Pure functions for handling a pasted tweakcn theme.
|
|
3
4
|
*
|
|
@@ -20,15 +21,15 @@
|
|
|
20
21
|
* silently stops working, with no error anywhere.
|
|
21
22
|
*/
|
|
22
23
|
type ThemeRecord = {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
id: string;
|
|
25
|
+
name: string;
|
|
26
|
+
css: string;
|
|
26
27
|
};
|
|
27
28
|
type ThemeValidationResult = {
|
|
28
|
-
|
|
29
|
+
ok: true;
|
|
29
30
|
} | {
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
ok: false;
|
|
32
|
+
reason: string;
|
|
32
33
|
};
|
|
33
34
|
/**
|
|
34
35
|
* Reduces a pasted tweakcn theme to just its `:root { ... }` and
|
|
@@ -45,5 +46,6 @@ declare function stripThemeCss(raw: string): string;
|
|
|
45
46
|
* no `:root` block at all.
|
|
46
47
|
*/
|
|
47
48
|
declare function validateThemeCss(css: string): ThemeValidationResult;
|
|
48
|
-
|
|
49
|
-
export {
|
|
49
|
+
//#endregion
|
|
50
|
+
export { ThemeRecord, ThemeValidationResult, stripThemeCss, validateThemeCss };
|
|
51
|
+
//# sourceMappingURL=theme-css.d.ts.map
|
package/dist/theme/theme-css.js
CHANGED
|
@@ -1,102 +1,102 @@
|
|
|
1
|
-
|
|
1
|
+
//#region src/theme/theme-css.ts
|
|
2
2
|
function parseTopLevelBlocks(src) {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
3
|
+
const n = src.length;
|
|
4
|
+
const blocks = [];
|
|
5
|
+
let i = 0;
|
|
6
|
+
const skipComment = (pos) => {
|
|
7
|
+
const end = src.indexOf("*/", pos + 2);
|
|
8
|
+
return end === -1 ? n : end + 2;
|
|
9
|
+
};
|
|
10
|
+
const skipString = (pos) => {
|
|
11
|
+
const quote = src[pos];
|
|
12
|
+
let j = pos + 1;
|
|
13
|
+
while (j < n && src[j] !== quote) {
|
|
14
|
+
if (src[j] === "\\") j++;
|
|
15
|
+
j++;
|
|
16
|
+
}
|
|
17
|
+
return j < n ? j + 1 : n;
|
|
18
|
+
};
|
|
19
|
+
while (i < n) {
|
|
20
|
+
while (i < n) {
|
|
21
|
+
const ch = src[i];
|
|
22
|
+
if (ch === " " || ch === " " || ch === "\n" || ch === "\r") {
|
|
23
|
+
i++;
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (src.startsWith("/*", i)) {
|
|
27
|
+
i = skipComment(i);
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
if (i >= n) break;
|
|
33
|
+
const statementStart = i;
|
|
34
|
+
let head = "";
|
|
35
|
+
while (i < n && src[i] !== "{" && src[i] !== ";") {
|
|
36
|
+
if (src.startsWith("/*", i)) {
|
|
37
|
+
i = skipComment(i);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (src[i] === "\"" || src[i] === "'") {
|
|
41
|
+
const strStart = i;
|
|
42
|
+
i = skipString(i);
|
|
43
|
+
head += src.slice(strStart, i);
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
head += src[i];
|
|
47
|
+
i++;
|
|
48
|
+
}
|
|
49
|
+
if (i >= n) break;
|
|
50
|
+
if (src[i] === ";") {
|
|
51
|
+
i++;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
let depth = 1;
|
|
55
|
+
let j = i + 1;
|
|
56
|
+
while (j < n && depth > 0) {
|
|
57
|
+
if (src.startsWith("/*", j)) {
|
|
58
|
+
j = skipComment(j);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (src[j] === "\"" || src[j] === "'") {
|
|
62
|
+
j = skipString(j);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (src[j] === "{") depth++;
|
|
66
|
+
else if (src[j] === "}") depth--;
|
|
67
|
+
j++;
|
|
68
|
+
}
|
|
69
|
+
blocks.push({
|
|
70
|
+
selector: head.trim(),
|
|
71
|
+
raw: src.slice(statementStart, j)
|
|
72
|
+
});
|
|
73
|
+
i = j;
|
|
74
|
+
}
|
|
75
|
+
return blocks;
|
|
73
76
|
}
|
|
74
77
|
function stripThemeCss(raw) {
|
|
75
|
-
|
|
76
|
-
const blocks = parseTopLevelBlocks(normalized);
|
|
77
|
-
const kept = blocks.filter((b) => b.selector === ":root" || b.selector === ".dark");
|
|
78
|
-
return kept.map((b) => b.raw).join("\n\n");
|
|
78
|
+
return parseTopLevelBlocks(raw.replace(/\r\n/g, "\n")).filter((b) => b.selector === ":root" || b.selector === ".dark").map((b) => b.raw).join("\n\n");
|
|
79
79
|
}
|
|
80
80
|
function validateThemeCss(css) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
81
|
+
const normalized = css.replace(/\r\n/g, "\n");
|
|
82
|
+
if (normalized.trim().length === 0) return {
|
|
83
|
+
ok: false,
|
|
84
|
+
reason: "Theme CSS is empty."
|
|
85
|
+
};
|
|
86
|
+
const blocks = parseTopLevelBlocks(normalized);
|
|
87
|
+
const hasDark = blocks.some((b) => b.selector === ".dark");
|
|
88
|
+
const hasRoot = blocks.some((b) => b.selector === ":root");
|
|
89
|
+
if (!hasDark) return {
|
|
90
|
+
ok: false,
|
|
91
|
+
reason: "Theme has no \".dark\" block. Without it, the theme's \":root\" values override the app's \".dark\" styles at equal CSS specificity, and dark mode silently stops working."
|
|
92
|
+
};
|
|
93
|
+
if (!hasRoot) return {
|
|
94
|
+
ok: false,
|
|
95
|
+
reason: "Theme has no \":root\" block — light-mode values are missing."
|
|
96
|
+
};
|
|
97
|
+
return { ok: true };
|
|
98
98
|
}
|
|
99
|
-
|
|
99
|
+
//#endregion
|
|
100
100
|
export { stripThemeCss, validateThemeCss };
|
|
101
|
-
|
|
101
|
+
|
|
102
102
|
//# sourceMappingURL=theme-css.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/theme/theme-css.ts"],"names":[],"mappings":"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","file":"theme-css.js","sourcesContent":["/**\n * Pure functions for handling a pasted tweakcn theme.\n *\n * tweakcn's \"Copy code\" button emits six constructs. Only two of them mean\n * anything once dropped into a browser at runtime (not build time, since\n * there is no Tailwind build step for pasted CSS):\n *\n * @import \"tailwindcss\"; -> 404s if kept (no such route at runtime)\n * @custom-variant dark (...); -> meaningless outside a Tailwind build\n * :root { ... } -> survives, sets light-mode custom properties\n * .dark { ... } -> survives, sets dark-mode custom properties\n * @theme inline { ... } -> meaningless outside a Tailwind build\n * @layer base { ...; @apply ...; } -> @apply is not real CSS, dead weight\n *\n * `stripThemeCss` throws away everything except the `:root` and `.dark`\n * blocks. `validateThemeCss` catches the one failure mode that is otherwise\n * invisible: a theme with no `.dark` block. `.dark` and `:root` share equal\n * specificity, so injecting a `:root`-only theme *after* the app's own\n * stylesheet makes its `:root` win over `html.dark`'s rules too — dark mode\n * silently stops working, with no error anywhere.\n */\n\nexport type ThemeRecord = { id: string; name: string; css: string };\n\nexport type ThemeValidationResult = { ok: true } | { ok: false; reason: string };\n\ninterface TopLevelBlock {\n /** The selector/at-rule head, trimmed (e.g. \":root\", \".dark\", \"@theme inline\"). */\n selector: string;\n /** The full source slice for this block, selector through closing brace. */\n raw: string;\n}\n\n/**\n * Walks the source once, splitting it into top-level statements. Any\n * statement terminated by `;` (e.g. `@import \"tailwindcss\";`,\n * `@custom-variant dark (&:is(.dark *));`) is dropped outright — it never\n * produces a block. Any statement with a `{ ... }` body is returned as a\n * block, with brace depth tracked through the body so a nested block (e.g.\n * `@layer base { * { ... } body { ... } }`) is consumed as a single unit\n * instead of stopping at its first inner `}`.\n *\n * Comments and quoted strings are skipped transparently everywhere braces\n * are counted, so a brace inside `/* ... *\\/` or inside a string value never\n * perturbs the depth count.\n *\n * Unclosed input (an opened `{`, `/*`, or quote that never finds its\n * closer) is NOT dropped: the depth/comment/string scan simply runs off\n * the end of the string, and the block gets pushed with its `raw` spanning\n * from the opening selector all the way to EOF — swallowing whatever\n * well-formed blocks would otherwise have followed it. Concretely: an\n * unclosed `:root {` earlier in the source will absorb a perfectly valid\n * `.dark { ... }` that comes after it, so `.dark` never becomes its own\n * top-level block. This is a deliberately-left gap, not a bug: the\n * consequence is caught downstream by `validateThemeCss`'s \"no .dark\"\n * check, so truncated/corrupt input fails closed rather than corrupting\n * silently. The one sharp edge is the *reason string* it fails with — a\n * clipboard read that came back truncated will be reported as \"no .dark\n * block\", not \"your paste was cut off\", which is worth remembering when\n * debugging a fetch step that feeds this function.\n */\nfunction parseTopLevelBlocks(src: string): TopLevelBlock[] {\n const n = src.length;\n const blocks: TopLevelBlock[] = [];\n let i = 0;\n\n const skipComment = (pos: number): number => {\n const end = src.indexOf('*/', pos + 2);\n return end === -1 ? n : end + 2;\n };\n\n const skipString = (pos: number): number => {\n const quote = src[pos];\n let j = pos + 1;\n while (j < n && src[j] !== quote) {\n if (src[j] === '\\\\') j++;\n j++;\n }\n return j < n ? j + 1 : n;\n };\n\n while (i < n) {\n // Skip whitespace and comments between statements.\n while (i < n) {\n const ch = src[i];\n if (ch === ' ' || ch === '\\t' || ch === '\\n' || ch === '\\r') {\n i++;\n continue;\n }\n if (src.startsWith('/*', i)) {\n i = skipComment(i);\n continue;\n }\n break;\n }\n if (i >= n) break;\n\n const statementStart = i;\n let head = '';\n\n // Scan the head (selector / at-rule prelude) up to a top-level `{` or `;`.\n while (i < n && src[i] !== '{' && src[i] !== ';') {\n if (src.startsWith('/*', i)) {\n i = skipComment(i);\n continue;\n }\n if (src[i] === '\"' || src[i] === \"'\") {\n const strStart = i;\n i = skipString(i);\n head += src.slice(strStart, i);\n continue;\n }\n head += src[i];\n i++;\n }\n\n // Ran off the end while scanning the head itself (no `{` or `;` ever\n // showed up) — this fragment genuinely IS dropped, since there is no\n // `{` to even start a block from. Contrast with an opened-but-unclosed\n // `{ ... }` body below, which is NOT dropped — see the block comment\n // on this function.\n if (i >= n) break;\n\n if (src[i] === ';') {\n i++; // e.g. `@import \"tailwindcss\";` — drop, no block produced.\n continue;\n }\n\n // src[i] === '{' — consume the whole block, tracking nested brace depth.\n let depth = 1;\n let j = i + 1;\n while (j < n && depth > 0) {\n if (src.startsWith('/*', j)) {\n j = skipComment(j);\n continue;\n }\n if (src[j] === '\"' || src[j] === \"'\") {\n j = skipString(j);\n continue;\n }\n if (src[j] === '{') depth++;\n else if (src[j] === '}') depth--;\n j++;\n }\n\n // Pushed unconditionally — if `depth` never reached 0 the loop above\n // only stopped because `j` hit `n` (EOF). That's intentional: see the\n // \"Unclosed input\" paragraph on this function's doc comment.\n blocks.push({ selector: head.trim(), raw: src.slice(statementStart, j) });\n i = j;\n }\n\n return blocks;\n}\n\n/**\n * Reduces a pasted tweakcn theme to just its `:root { ... }` and\n * `.dark { ... }` blocks, in source order. Everything else (imports,\n * `@custom-variant`, `@theme inline`, `@layer base`) is dropped. Idempotent:\n * running it again on its own output is a no-op.\n */\nexport function stripThemeCss(raw: string): string {\n const normalized = raw.replace(/\\r\\n/g, '\\n');\n const blocks = parseTopLevelBlocks(normalized);\n const kept = blocks.filter((b) => b.selector === ':root' || b.selector === '.dark');\n return kept.map((b) => b.raw).join('\\n\\n');\n}\n\n/**\n * Validates that a (normally already-stripped) theme is safe to inject.\n * The load-bearing check is `.dark`: without it, a theme's `:root` values\n * override the app's `.dark` rules at equal specificity, and dark mode\n * breaks silently. Also rejects empty input and (defensively) a theme with\n * no `:root` block at all.\n */\nexport function validateThemeCss(css: string): ThemeValidationResult {\n const normalized = css.replace(/\\r\\n/g, '\\n');\n if (normalized.trim().length === 0) {\n return { ok: false, reason: 'Theme CSS is empty.' };\n }\n\n const blocks = parseTopLevelBlocks(normalized);\n const hasDark = blocks.some((b) => b.selector === '.dark');\n const hasRoot = blocks.some((b) => b.selector === ':root');\n\n if (!hasDark) {\n return {\n ok: false,\n reason:\n 'Theme has no \".dark\" block. Without it, the theme\\'s \":root\" values ' +\n 'override the app\\'s \".dark\" styles at equal CSS specificity, and dark ' +\n 'mode silently stops working.',\n };\n }\n\n if (!hasRoot) {\n return { ok: false, reason: 'Theme has no \":root\" block — light-mode values are missing.' };\n }\n\n return { ok: true };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"theme-css.js","names":[],"sources":["../../src/theme/theme-css.ts"],"sourcesContent":["/**\n * Pure functions for handling a pasted tweakcn theme.\n *\n * tweakcn's \"Copy code\" button emits six constructs. Only two of them mean\n * anything once dropped into a browser at runtime (not build time, since\n * there is no Tailwind build step for pasted CSS):\n *\n * @import \"tailwindcss\"; -> 404s if kept (no such route at runtime)\n * @custom-variant dark (...); -> meaningless outside a Tailwind build\n * :root { ... } -> survives, sets light-mode custom properties\n * .dark { ... } -> survives, sets dark-mode custom properties\n * @theme inline { ... } -> meaningless outside a Tailwind build\n * @layer base { ...; @apply ...; } -> @apply is not real CSS, dead weight\n *\n * `stripThemeCss` throws away everything except the `:root` and `.dark`\n * blocks. `validateThemeCss` catches the one failure mode that is otherwise\n * invisible: a theme with no `.dark` block. `.dark` and `:root` share equal\n * specificity, so injecting a `:root`-only theme *after* the app's own\n * stylesheet makes its `:root` win over `html.dark`'s rules too — dark mode\n * silently stops working, with no error anywhere.\n */\n\nexport type ThemeRecord = { id: string; name: string; css: string };\n\nexport type ThemeValidationResult = { ok: true } | { ok: false; reason: string };\n\ninterface TopLevelBlock {\n /** The selector/at-rule head, trimmed (e.g. \":root\", \".dark\", \"@theme inline\"). */\n selector: string;\n /** The full source slice for this block, selector through closing brace. */\n raw: string;\n}\n\n/**\n * Walks the source once, splitting it into top-level statements. Any\n * statement terminated by `;` (e.g. `@import \"tailwindcss\";`,\n * `@custom-variant dark (&:is(.dark *));`) is dropped outright — it never\n * produces a block. Any statement with a `{ ... }` body is returned as a\n * block, with brace depth tracked through the body so a nested block (e.g.\n * `@layer base { * { ... } body { ... } }`) is consumed as a single unit\n * instead of stopping at its first inner `}`.\n *\n * Comments and quoted strings are skipped transparently everywhere braces\n * are counted, so a brace inside `/* ... *\\/` or inside a string value never\n * perturbs the depth count.\n *\n * Unclosed input (an opened `{`, `/*`, or quote that never finds its\n * closer) is NOT dropped: the depth/comment/string scan simply runs off\n * the end of the string, and the block gets pushed with its `raw` spanning\n * from the opening selector all the way to EOF — swallowing whatever\n * well-formed blocks would otherwise have followed it. Concretely: an\n * unclosed `:root {` earlier in the source will absorb a perfectly valid\n * `.dark { ... }` that comes after it, so `.dark` never becomes its own\n * top-level block. This is a deliberately-left gap, not a bug: the\n * consequence is caught downstream by `validateThemeCss`'s \"no .dark\"\n * check, so truncated/corrupt input fails closed rather than corrupting\n * silently. The one sharp edge is the *reason string* it fails with — a\n * clipboard read that came back truncated will be reported as \"no .dark\n * block\", not \"your paste was cut off\", which is worth remembering when\n * debugging a fetch step that feeds this function.\n */\nfunction parseTopLevelBlocks(src: string): TopLevelBlock[] {\n const n = src.length;\n const blocks: TopLevelBlock[] = [];\n let i = 0;\n\n const skipComment = (pos: number): number => {\n const end = src.indexOf('*/', pos + 2);\n return end === -1 ? n : end + 2;\n };\n\n const skipString = (pos: number): number => {\n const quote = src[pos];\n let j = pos + 1;\n while (j < n && src[j] !== quote) {\n if (src[j] === '\\\\') j++;\n j++;\n }\n return j < n ? j + 1 : n;\n };\n\n while (i < n) {\n // Skip whitespace and comments between statements.\n while (i < n) {\n const ch = src[i];\n if (ch === ' ' || ch === '\\t' || ch === '\\n' || ch === '\\r') {\n i++;\n continue;\n }\n if (src.startsWith('/*', i)) {\n i = skipComment(i);\n continue;\n }\n break;\n }\n if (i >= n) break;\n\n const statementStart = i;\n let head = '';\n\n // Scan the head (selector / at-rule prelude) up to a top-level `{` or `;`.\n while (i < n && src[i] !== '{' && src[i] !== ';') {\n if (src.startsWith('/*', i)) {\n i = skipComment(i);\n continue;\n }\n if (src[i] === '\"' || src[i] === \"'\") {\n const strStart = i;\n i = skipString(i);\n head += src.slice(strStart, i);\n continue;\n }\n head += src[i];\n i++;\n }\n\n // Ran off the end while scanning the head itself (no `{` or `;` ever\n // showed up) — this fragment genuinely IS dropped, since there is no\n // `{` to even start a block from. Contrast with an opened-but-unclosed\n // `{ ... }` body below, which is NOT dropped — see the block comment\n // on this function.\n if (i >= n) break;\n\n if (src[i] === ';') {\n i++; // e.g. `@import \"tailwindcss\";` — drop, no block produced.\n continue;\n }\n\n // src[i] === '{' — consume the whole block, tracking nested brace depth.\n let depth = 1;\n let j = i + 1;\n while (j < n && depth > 0) {\n if (src.startsWith('/*', j)) {\n j = skipComment(j);\n continue;\n }\n if (src[j] === '\"' || src[j] === \"'\") {\n j = skipString(j);\n continue;\n }\n if (src[j] === '{') depth++;\n else if (src[j] === '}') depth--;\n j++;\n }\n\n // Pushed unconditionally — if `depth` never reached 0 the loop above\n // only stopped because `j` hit `n` (EOF). That's intentional: see the\n // \"Unclosed input\" paragraph on this function's doc comment.\n blocks.push({ selector: head.trim(), raw: src.slice(statementStart, j) });\n i = j;\n }\n\n return blocks;\n}\n\n/**\n * Reduces a pasted tweakcn theme to just its `:root { ... }` and\n * `.dark { ... }` blocks, in source order. Everything else (imports,\n * `@custom-variant`, `@theme inline`, `@layer base`) is dropped. Idempotent:\n * running it again on its own output is a no-op.\n */\nexport function stripThemeCss(raw: string): string {\n const normalized = raw.replace(/\\r\\n/g, '\\n');\n const blocks = parseTopLevelBlocks(normalized);\n const kept = blocks.filter((b) => b.selector === ':root' || b.selector === '.dark');\n return kept.map((b) => b.raw).join('\\n\\n');\n}\n\n/**\n * Validates that a (normally already-stripped) theme is safe to inject.\n * The load-bearing check is `.dark`: without it, a theme's `:root` values\n * override the app's `.dark` rules at equal specificity, and dark mode\n * breaks silently. Also rejects empty input and (defensively) a theme with\n * no `:root` block at all.\n */\nexport function validateThemeCss(css: string): ThemeValidationResult {\n const normalized = css.replace(/\\r\\n/g, '\\n');\n if (normalized.trim().length === 0) {\n return { ok: false, reason: 'Theme CSS is empty.' };\n }\n\n const blocks = parseTopLevelBlocks(normalized);\n const hasDark = blocks.some((b) => b.selector === '.dark');\n const hasRoot = blocks.some((b) => b.selector === ':root');\n\n if (!hasDark) {\n return {\n ok: false,\n reason:\n 'Theme has no \".dark\" block. Without it, the theme\\'s \":root\" values ' +\n 'override the app\\'s \".dark\" styles at equal CSS specificity, and dark ' +\n 'mode silently stops working.',\n };\n }\n\n if (!hasRoot) {\n return { ok: false, reason: 'Theme has no \":root\" block — light-mode values are missing.' };\n }\n\n return { ok: true };\n}\n"],"mappings":";AA6DA,SAAS,oBAAoB,KAA8B;CACzD,MAAM,IAAI,IAAI;CACd,MAAM,SAA0B,CAAC;CACjC,IAAI,IAAI;CAER,MAAM,eAAe,QAAwB;EAC3C,MAAM,MAAM,IAAI,QAAQ,MAAM,MAAM,CAAC;EACrC,OAAO,QAAQ,KAAK,IAAI,MAAM;CAChC;CAEA,MAAM,cAAc,QAAwB;EAC1C,MAAM,QAAQ,IAAI;EAClB,IAAI,IAAI,MAAM;EACd,OAAO,IAAI,KAAK,IAAI,OAAO,OAAO;GAChC,IAAI,IAAI,OAAO,MAAM;GACrB;EACF;EACA,OAAO,IAAI,IAAI,IAAI,IAAI;CACzB;CAEA,OAAO,IAAI,GAAG;EAEZ,OAAO,IAAI,GAAG;GACZ,MAAM,KAAK,IAAI;GACf,IAAI,OAAO,OAAO,OAAO,OAAQ,OAAO,QAAQ,OAAO,MAAM;IAC3D;IACA;GACF;GACA,IAAI,IAAI,WAAW,MAAM,CAAC,GAAG;IAC3B,IAAI,YAAY,CAAC;IACjB;GACF;GACA;EACF;EACA,IAAI,KAAK,GAAG;EAEZ,MAAM,iBAAiB;EACvB,IAAI,OAAO;EAGX,OAAO,IAAI,KAAK,IAAI,OAAO,OAAO,IAAI,OAAO,KAAK;GAChD,IAAI,IAAI,WAAW,MAAM,CAAC,GAAG;IAC3B,IAAI,YAAY,CAAC;IACjB;GACF;GACA,IAAI,IAAI,OAAO,QAAO,IAAI,OAAO,KAAK;IACpC,MAAM,WAAW;IACjB,IAAI,WAAW,CAAC;IAChB,QAAQ,IAAI,MAAM,UAAU,CAAC;IAC7B;GACF;GACA,QAAQ,IAAI;GACZ;EACF;EAOA,IAAI,KAAK,GAAG;EAEZ,IAAI,IAAI,OAAO,KAAK;GAClB;GACA;EACF;EAGA,IAAI,QAAQ;EACZ,IAAI,IAAI,IAAI;EACZ,OAAO,IAAI,KAAK,QAAQ,GAAG;GACzB,IAAI,IAAI,WAAW,MAAM,CAAC,GAAG;IAC3B,IAAI,YAAY,CAAC;IACjB;GACF;GACA,IAAI,IAAI,OAAO,QAAO,IAAI,OAAO,KAAK;IACpC,IAAI,WAAW,CAAC;IAChB;GACF;GACA,IAAI,IAAI,OAAO,KAAK;QACf,IAAI,IAAI,OAAO,KAAK;GACzB;EACF;EAKA,OAAO,KAAK;GAAE,UAAU,KAAK,KAAK;GAAG,KAAK,IAAI,MAAM,gBAAgB,CAAC;EAAE,CAAC;EACxE,IAAI;CACN;CAEA,OAAO;AACT;AAQA,SAAgB,cAAc,KAAqB;CAIjD,OAFe,oBADI,IAAI,QAAQ,SAAS,IACI,CAC1B,CAAC,CAAC,QAAQ,MAAM,EAAE,aAAa,WAAW,EAAE,aAAa,OACjE,CAAC,CAAC,KAAK,MAAM,EAAE,GAAG,CAAC,CAAC,KAAK,MAAM;AAC3C;AASA,SAAgB,iBAAiB,KAAoC;CACnE,MAAM,aAAa,IAAI,QAAQ,SAAS,IAAI;CAC5C,IAAI,WAAW,KAAK,CAAC,CAAC,WAAW,GAC/B,OAAO;EAAE,IAAI;EAAO,QAAQ;CAAsB;CAGpD,MAAM,SAAS,oBAAoB,UAAU;CAC7C,MAAM,UAAU,OAAO,MAAM,MAAM,EAAE,aAAa,OAAO;CACzD,MAAM,UAAU,OAAO,MAAM,MAAM,EAAE,aAAa,OAAO;CAEzD,IAAI,CAAC,SACH,OAAO;EACL,IAAI;EACJ,QACE;CAGJ;CAGF,IAAI,CAAC,SACH,OAAO;EAAE,IAAI;EAAO,QAAQ;CAA8D;CAG5F,OAAO,EAAE,IAAI,KAAK;AACpB"}
|