fix(theme): update theme class naming and scoping logic

- Changed theme class names from `.theme-*` to `.Theme--*` for consistency.
- Updated scoping logic to prevent scoping of `.Theme--` classes, ensuring they remain global.
- Enhanced theme store logic to check for existing `.Theme--` classes before applying new theme classes, preventing conflicts.
- Adjusted class cleaning logic to retain `.Theme--` classes when necessary.
This commit is contained in:
Eli Bosley
2025-11-18 13:29:39 -05:00
parent ee0f240233
commit b28ef1ea33
3 changed files with 37 additions and 14 deletions

View File

@@ -5,8 +5,7 @@
*/ */
/* Default/White Theme */ /* Default/White Theme */
:root, .Theme--white {
.theme-white {
--header-text-primary: #ffffff; --header-text-primary: #ffffff;
--header-text-secondary: #999999; --header-text-secondary: #999999;
--header-background-color: #1c1b1b; --header-background-color: #1c1b1b;
@@ -20,8 +19,8 @@
} }
/* Black Theme */ /* Black Theme */
.theme-black, .Theme--black,
.theme-black.dark { .Theme--black.dark {
--header-text-primary: #1c1b1b; --header-text-primary: #1c1b1b;
--header-text-secondary: #999999; --header-text-secondary: #999999;
--header-background-color: #f2f2f2; --header-background-color: #f2f2f2;
@@ -35,7 +34,7 @@
} }
/* Gray Theme */ /* Gray Theme */
.theme-gray { .Theme--gray {
--header-text-primary: #ffffff; --header-text-primary: #ffffff;
--header-text-secondary: #999999; --header-text-secondary: #999999;
--header-background-color: #1c1b1b; --header-background-color: #1c1b1b;
@@ -49,7 +48,7 @@
} }
/* Azure Theme */ /* Azure Theme */
.theme-azure { .Theme--azure {
--header-text-primary: #1c1b1b; --header-text-primary: #1c1b1b;
--header-text-secondary: #999999; --header-text-secondary: #999999;
--header-background-color: #f2f2f2; --header-background-color: #f2f2f2;

View File

@@ -35,7 +35,8 @@ const DEFAULT_INCLUDE_ROOT = true;
const KEYFRAME_AT_RULES = new Set(['keyframes']); const KEYFRAME_AT_RULES = new Set(['keyframes']);
const NON_SCOPED_AT_RULES = new Set(['font-face', 'page']); const NON_SCOPED_AT_RULES = new Set(['font-face', 'page']);
const MERGE_WITH_SCOPE_PATTERNS: RegExp[] = [/^\.theme-/, /^\.has-custom-/, /^\.dark\b/]; const MERGE_WITH_SCOPE_PATTERNS: RegExp[] = [/^\.has-custom-/, /^\.dark\b/];
const UNSCOPED_PATTERNS: RegExp[] = [/^\.Theme--/];
function shouldScopeRule(rule: Rule, targetLayers: Set<string>, includeRootRules: boolean): boolean { function shouldScopeRule(rule: Rule, targetLayers: Set<string>, includeRootRules: boolean): boolean {
const hasSelectorString = typeof rule.selector === 'string' && rule.selector.length > 0; const hasSelectorString = typeof rule.selector === 'string' && rule.selector.length > 0;
@@ -104,6 +105,12 @@ function prefixSelector(selector: string, scope: string): string {
return trimmed; return trimmed;
} }
// Do not scope Theme-- classes - they should remain global
const firstToken = trimmed.split(/[\s>+~]/, 1)[0] ?? '';
if (!firstToken.includes('\\:') && UNSCOPED_PATTERNS.some((pattern) => pattern.test(firstToken))) {
return trimmed;
}
if (trimmed === ':root') { if (trimmed === ':root') {
return scope; return scope;
} }
@@ -112,7 +119,6 @@ function prefixSelector(selector: string, scope: string): string {
return `${scope}${trimmed.slice(':root'.length)}`; return `${scope}${trimmed.slice(':root'.length)}`;
} }
const firstToken = trimmed.split(/[\s>+~]/, 1)[0] ?? '';
const shouldMergeWithScope = const shouldMergeWithScope =
!firstToken.includes('\\:') && MERGE_WITH_SCOPE_PATTERNS.some((pattern) => pattern.test(firstToken)); !firstToken.includes('\\:') && MERGE_WITH_SCOPE_PATTERNS.some((pattern) => pattern.test(firstToken));

View File

@@ -168,6 +168,11 @@ export const useThemeStore = defineStore(
const setCssVars = () => { const setCssVars = () => {
const selectedTheme = theme.value.name; const selectedTheme = theme.value.name;
// Check if Unraid PHP has already set a Theme-- class
const hasExistingThemeClass =
typeof document !== 'undefined' &&
Array.from(document.documentElement.classList).some((cls) => cls.startsWith('Theme--'));
// Prepare Tailwind v4 theme classes // Prepare Tailwind v4 theme classes
const themeClasses: string[] = []; const themeClasses: string[] = [];
const customClasses: string[] = []; const customClasses: string[] = [];
@@ -177,8 +182,10 @@ export const useThemeStore = defineStore(
themeClasses.push('dark'); themeClasses.push('dark');
} }
// Apply theme-specific class for Tailwind v4 theme variants // Only apply theme-specific class if Unraid PHP hasn't already set it
themeClasses.push(`theme-${selectedTheme}`); if (!hasExistingThemeClass) {
themeClasses.push(`Theme--${selectedTheme}`);
}
// Only set CSS variables for dynamic/user-configured values from GraphQL // Only set CSS variables for dynamic/user-configured values from GraphQL
// Static theme values are handled by Tailwind v4 theme classes in @tailwind-shared // Static theme values are handled by Tailwind v4 theme classes in @tailwind-shared
@@ -220,22 +227,33 @@ export const useThemeStore = defineStore(
...Array.from(document.querySelectorAll<HTMLElement>('.unapi')), ...Array.from(document.querySelectorAll<HTMLElement>('.unapi')),
]; ];
const cleanClassList = (classList: string) => const cleanClassList = (classList: string, isDocumentElement: boolean) => {
classList // Don't remove Theme-- classes from documentElement if Unraid PHP set them
if (isDocumentElement && hasExistingThemeClass) {
return classList
.split(' ')
.filter((c) => c !== 'dark' && !c.startsWith('has-custom-') && c !== 'has-banner-gradient')
.filter(Boolean)
.join(' ');
}
// For .unapi roots or when we're managing the theme class, clean everything
return classList
.split(' ') .split(' ')
.filter( .filter(
(c) => (c) =>
!c.startsWith('theme-') && !c.startsWith('Theme--') &&
c !== 'dark' && c !== 'dark' &&
!c.startsWith('has-custom-') && !c.startsWith('has-custom-') &&
c !== 'has-banner-gradient' c !== 'has-banner-gradient'
) )
.filter(Boolean) .filter(Boolean)
.join(' '); .join(' ');
};
// Apply theme and custom classes to html element and all .unapi roots // Apply theme and custom classes to html element and all .unapi roots
scopedTargets.forEach((target) => { scopedTargets.forEach((target) => {
target.className = cleanClassList(target.className); const isDocumentElement = target === document.documentElement;
target.className = cleanClassList(target.className, isDocumentElement);
[...themeClasses, ...customClasses].forEach((cls) => target.classList.add(cls)); [...themeClasses, ...customClasses].forEach((cls) => target.classList.add(cls));
if (darkMode.value) { if (darkMode.value) {