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

View File

@@ -35,7 +35,8 @@ const DEFAULT_INCLUDE_ROOT = true;
const KEYFRAME_AT_RULES = new Set(['keyframes']);
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 {
const hasSelectorString = typeof rule.selector === 'string' && rule.selector.length > 0;
@@ -104,6 +105,12 @@ function prefixSelector(selector: string, scope: string): string {
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') {
return scope;
}
@@ -112,7 +119,6 @@ function prefixSelector(selector: string, scope: string): string {
return `${scope}${trimmed.slice(':root'.length)}`;
}
const firstToken = trimmed.split(/[\s>+~]/, 1)[0] ?? '';
const shouldMergeWithScope =
!firstToken.includes('\\:') && MERGE_WITH_SCOPE_PATTERNS.some((pattern) => pattern.test(firstToken));

View File

@@ -168,6 +168,11 @@ export const useThemeStore = defineStore(
const setCssVars = () => {
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
const themeClasses: string[] = [];
const customClasses: string[] = [];
@@ -177,8 +182,10 @@ export const useThemeStore = defineStore(
themeClasses.push('dark');
}
// Apply theme-specific class for Tailwind v4 theme variants
themeClasses.push(`theme-${selectedTheme}`);
// Only apply theme-specific class if Unraid PHP hasn't already set it
if (!hasExistingThemeClass) {
themeClasses.push(`Theme--${selectedTheme}`);
}
// Only set CSS variables for dynamic/user-configured values from GraphQL
// 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')),
];
const cleanClassList = (classList: string) =>
classList
const cleanClassList = (classList: string, isDocumentElement: boolean) => {
// 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(' ')
.filter(
(c) =>
!c.startsWith('theme-') &&
!c.startsWith('Theme--') &&
c !== 'dark' &&
!c.startsWith('has-custom-') &&
c !== 'has-banner-gradient'
)
.filter(Boolean)
.join(' ');
};
// Apply theme and custom classes to html element and all .unapi roots
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));
if (darkMode.value) {