Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
e0ff6e3
fix: enhance dark mode support in theme handling
elibosley Nov 24, 2025
1b59aa0
test: update theme store tests to initialize dark mode from CSS variable
elibosley Nov 24, 2025
be3b9ae
test: simplify theme store test initialization
elibosley Nov 24, 2025
d2d9264
refactor: enhance dark mode handling and theme initialization
elibosley Nov 25, 2025
73a32e2
feat: enhance banner gradient customization and update related tests
elibosley Nov 25, 2025
2203f95
test: add theme store test for banner gradient configuration
elibosley Nov 25, 2025
29c8fd8
test: update UserProfile tests to utilize theme store methods
elibosley Nov 25, 2025
9f98ddd
test: enhance theme store tests with direct CSS variable manipulation
elibosley Nov 25, 2025
4a14897
feat: add customization mutations for theme management
elibosley Nov 26, 2025
1588bdc
feat: implement setTheme mutation for dynamic theme management
elibosley Dec 9, 2025
3baa19e
feat: centralize dark mode detection with isDarkModeActive utility
elibosley Dec 9, 2025
bebc907
test: enhance useTeleport tests for dark mode functionality
elibosley Dec 9, 2025
309aa5c
test: enhance theme store tests with dark mode detection and cleanup
elibosley Dec 9, 2025
7db83af
test: update theme store tests for banner gradient configuration
elibosley Dec 9, 2025
8a694d2
refactor: update banner visibility logic and CSS variable handling
elibosley Dec 15, 2025
e0a696a
fix: improve manifest content handling in WebComponentsExtractor
elibosley Dec 15, 2025
610a742
fix: improve manifest content handling in WebComponentsExtractor
elibosley Dec 15, 2025
a314f5c
refactor: simplify banner gradient logic in theme store
elibosley Dec 15, 2025
c1e1a29
test: update UserProfile and theme store tests for banner gradient logic
elibosley Dec 15, 2025
b46faee
refactor: update CSS variables for theme gradients and shadows
elibosley Dec 15, 2025
43e83e8
refactor: update header gradient handling for improved consistency
elibosley Dec 15, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 0 additions & 9 deletions @tailwind-shared/base-utilities.css
Original file line number Diff line number Diff line change
Expand Up @@ -63,15 +63,6 @@
*/

.unapi {
--color-alpha: #1c1b1b;
--color-beta: #f2f2f2;
--color-gamma: #999999;
--color-gamma-opaque: rgba(153, 153, 153, 0.5);
--color-customgradient-start: rgba(242, 242, 242, 0);
--color-customgradient-end: rgba(242, 242, 242, 0.85);
--shadow-beta: 0 25px 50px -12px rgba(242, 242, 242, 0.15);
--ring-offset-shadow: 0 0 var(--color-beta);
--ring-shadow: 0 0 var(--color-beta);
}

.unapi button:not(:disabled),
Expand Down
23 changes: 22 additions & 1 deletion @tailwind-shared/theme-variants.css
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@
--color-beta: #1c1b1b;
--color-gamma: #ffffff;
--color-gamma-opaque: rgba(255, 255, 255, 0.3);
--color-header-gradient-start: color-mix(in srgb, var(--header-background-color) 0%, transparent);
--color-header-gradient-end: color-mix(in srgb, var(--header-background-color) 100%, transparent);
--shadow-beta: 0 25px 50px -12px color-mix(in srgb, var(--color-beta) 15%, transparent);
--ring-offset-shadow: 0 0 var(--color-beta);
--ring-shadow: 0 0 var(--color-beta);
}

/* Black Theme */
Expand All @@ -21,15 +26,26 @@
--color-beta: #f2f2f2;
--color-gamma: #1c1b1b;
--color-gamma-opaque: rgba(28, 27, 27, 0.3);
--color-header-gradient-start: color-mix(in srgb, var(--header-background-color) 0%, transparent);
--color-header-gradient-end: color-mix(in srgb, var(--header-background-color) 100%, transparent);
--shadow-beta: 0 25px 50px -12px color-mix(in srgb, var(--color-beta) 15%, transparent);
--ring-offset-shadow: 0 0 var(--color-beta);
--ring-shadow: 0 0 var(--color-beta);
}

/* Gray Theme */
.Theme--gray {
.Theme--gray,
.Theme--gray.dark {
--color-border: #383735;
--color-alpha: #ff8c2f;
--color-beta: #383735;
--color-gamma: #ffffff;
--color-gamma-opaque: rgba(255, 255, 255, 0.3);
--color-header-gradient-start: color-mix(in srgb, var(--header-background-color) 0%, transparent);
--color-header-gradient-end: color-mix(in srgb, var(--header-background-color) 100%, transparent);
--shadow-beta: 0 25px 50px -12px color-mix(in srgb, var(--color-beta) 15%, transparent);
--ring-offset-shadow: 0 0 var(--color-beta);
--ring-shadow: 0 0 var(--color-beta);
}

/* Azure Theme */
Expand All @@ -39,6 +55,11 @@
--color-beta: #e7f2f8;
--color-gamma: #336699;
--color-gamma-opaque: rgba(51, 102, 153, 0.3);
--color-header-gradient-start: color-mix(in srgb, var(--header-background-color) 0%, transparent);
--color-header-gradient-end: color-mix(in srgb, var(--header-background-color) 100%, transparent);
--shadow-beta: 0 25px 50px -12px color-mix(in srgb, var(--color-beta) 15%, transparent);
--ring-offset-shadow: 0 0 var(--color-beta);
--ring-shadow: 0 0 var(--color-beta);
}

/* Dark Mode Overrides */
Expand Down
26 changes: 18 additions & 8 deletions api/generated-schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -944,6 +944,23 @@ input UpdateApiKeyInput {
permissions: [AddPermissionInput!]
}

"""Customization related mutations"""
type CustomizationMutations {
"""Update the UI theme (writes dynamix.cfg)"""
setTheme(
"""Theme to apply"""
theme: ThemeName!
): Theme!
}

"""The theme name"""
enum ThemeName {
azure
black
gray
white
}

"""
Parity check related mutations, WIP, response types and functionaliy will change
"""
Expand Down Expand Up @@ -1042,14 +1059,6 @@ type Theme {
headerSecondaryTextColor: String
}

"""The theme name"""
enum ThemeName {
azure
black
gray
white
}

type ExplicitStatusItem {
name: String!
updateStatus: UpdateStatus!
Expand Down Expand Up @@ -2449,6 +2458,7 @@ type Mutation {
vm: VmMutations!
parityCheck: ParityCheckMutations!
apiKey: ApiKeyMutations!
customization: CustomizationMutations!
rclone: RCloneMutations!
createDockerFolder(name: String!, parentId: String, childrenIds: [String!]): ResolvedOrganizerV1!
setDockerFolderChildren(folderId: String, childrenIds: [String!]!): ResolvedOrganizerV1!
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { Module } from '@nestjs/common';

import { CustomizationMutationsResolver } from '@app/unraid-api/graph/resolvers/customization/customization.mutations.resolver.js';
import { CustomizationResolver } from '@app/unraid-api/graph/resolvers/customization/customization.resolver.js';
import { CustomizationService } from '@app/unraid-api/graph/resolvers/customization/customization.service.js';

@Module({
providers: [CustomizationService, CustomizationResolver],
providers: [CustomizationService, CustomizationResolver, CustomizationMutationsResolver],
exports: [CustomizationService],
})
export class CustomizationModule {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Args, ResolveField, Resolver } from '@nestjs/graphql';

import { AuthAction, Resource } from '@unraid/shared/graphql.model.js';
import { UsePermissions } from '@unraid/shared/use-permissions.directive.js';

import { CustomizationService } from '@app/unraid-api/graph/resolvers/customization/customization.service.js';
import { Theme, ThemeName } from '@app/unraid-api/graph/resolvers/customization/theme.model.js';
import { CustomizationMutations } from '@app/unraid-api/graph/resolvers/mutation/mutation.model.js';

@Resolver(() => CustomizationMutations)
export class CustomizationMutationsResolver {
constructor(private readonly customizationService: CustomizationService) {}

@ResolveField(() => Theme, { description: 'Update the UI theme (writes dynamix.cfg)' })
@UsePermissions({
action: AuthAction.UPDATE_ANY,
resource: Resource.CUSTOMIZATIONS,
})
async setTheme(
@Args('theme', { type: () => ThemeName, description: 'Theme to apply' })
theme: ThemeName
): Promise<Theme> {
return this.customizationService.setTheme(theme);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import * as ini from 'ini';

import { emcmd } from '@app/core/utils/clients/emcmd.js';
import { fileExists } from '@app/core/utils/files/file-exists.js';
import { loadDynamixConfigFromDiskSync } from '@app/store/actions/load-dynamix-config-file.js';
import { getters, store } from '@app/store/index.js';
import { updateDynamixConfig } from '@app/store/modules/dynamix.js';
import {
ActivationCode,
PublicPartnerInfo,
Expand Down Expand Up @@ -466,4 +468,16 @@ export class CustomizationService implements OnModuleInit {
showHeaderDescription: descriptionShow === 'yes',
};
}

public async setTheme(theme: ThemeName): Promise<Theme> {
this.logger.log(`Updating theme to ${theme}`);
await this.updateCfgFile(this.configFile, 'display', { theme });

// Refresh in-memory store so subsequent reads get the new theme without a restart
const paths = getters.paths();
const updatedConfig = loadDynamixConfigFromDiskSync(paths['dynamix-config']);
store.dispatch(updateDynamixConfig(updatedConfig));

return this.getTheme();
}
}
8 changes: 8 additions & 0 deletions api/src/unraid-api/graph/resolvers/mutation/mutation.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ export class VmMutations {}
})
export class ApiKeyMutations {}

@ObjectType({
description: 'Customization related mutations',
})
export class CustomizationMutations {}

@ObjectType({
description: 'Parity check related mutations, WIP, response types and functionaliy will change',
})
Expand Down Expand Up @@ -54,6 +59,9 @@ export class RootMutations {
@Field(() => ApiKeyMutations, { description: 'API Key related mutations' })
apiKey: ApiKeyMutations = new ApiKeyMutations();

@Field(() => CustomizationMutations, { description: 'Customization related mutations' })
customization: CustomizationMutations = new CustomizationMutations();

@Field(() => ParityCheckMutations, { description: 'Parity check related mutations' })
parityCheck: ParityCheckMutations = new ParityCheckMutations();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Mutation, Resolver } from '@nestjs/graphql';
import {
ApiKeyMutations,
ArrayMutations,
CustomizationMutations,
DockerMutations,
ParityCheckMutations,
RCloneMutations,
Expand Down Expand Up @@ -37,6 +38,11 @@ export class RootMutationsResolver {
return new ApiKeyMutations();
}

@Mutation(() => CustomizationMutations, { name: 'customization' })
customization(): CustomizationMutations {
return new CustomizationMutations();
}

@Mutation(() => RCloneMutations, { name: 'rclone' })
rclone(): RCloneMutations {
return new RCloneMutations();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,12 @@ private function sanitizeForId(string $input): string
public function getManifestContents(string $manifestPath): array
{
$contents = @file_get_contents($manifestPath);
return $contents ? json_decode($contents, true) : [];
if (!$contents) {
return [];
}

$decoded = json_decode($contents, true);
return is_array($decoded) ? $decoded : [];
}

private function processManifestFiles(): string
Expand Down Expand Up @@ -209,6 +214,11 @@ private function getDisplayThemeVars(): ?array
}

$theme = strtolower(trim($display['theme'] ?? ''));
$darkThemes = ['gray', 'black'];
$isDarkMode = in_array($theme, $darkThemes, true);
$vars['--theme-dark-mode'] = $isDarkMode ? '1' : '0';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i get antipattern/red flag vibes from this (ie multiple ways to set dark mode, multiple places you have to consider to debug themes--which could step on each others' toes)

i suspect this is in the spirit of a css implementation of the dark mode recognition we were doing in js, though.

for my own context, what are the limitations of purely setting a css var palette based on theme? is it the use of dark: styles in our component code & component dependencies?

$vars['--theme-name'] = $theme ?: 'white';

if ($theme === 'white') {
if (!$textPrimary) {
$vars['--header-text-primary'] = 'var(--inverse-text-color, #ffffff)';
Expand All @@ -218,19 +228,35 @@ private function getDisplayThemeVars(): ?array
}
}

// Unraid WebGUI stores banner enablement as a non-empty `display['banner']` value
// (typically the banner file name/path).
$shouldShowBanner = !empty($display['banner']);
$bgColor = $this->normalizeHex($display['background'] ?? null);
if ($bgColor) {
$vars['--header-background-color'] = $bgColor;
$vars['--header-gradient-start'] = $this->hexToRgba($bgColor, 0);
$vars['--header-gradient-end'] = $this->hexToRgba($bgColor, 0.7);
// Only set gradient variables if banner image is enabled
if ($shouldShowBanner) {
$vars['--header-gradient-start'] = $this->hexToRgba($bgColor, 0);
$vars['--header-gradient-end'] = $this->hexToRgba($bgColor, 1);
}
}

$shouldShowBannerGradient = ($display['showBannerGradient'] ?? '') === 'yes';
if ($shouldShowBannerGradient) {
$start = $vars['--header-gradient-start'] ?? 'rgba(0, 0, 0, 0)';
$end = $vars['--header-gradient-end'] ?? 'rgba(0, 0, 0, 0.7)';
if ($shouldShowBanner && $shouldShowBannerGradient) {
// If the user didn't set a custom background color, prefer existing theme defaults instead of falling back to black.
if (!isset($vars['--header-gradient-start'])) {
$vars['--header-gradient-start'] = 'var(--color-header-gradient-start, rgba(242, 242, 242, 0))';
}
if (!isset($vars['--header-gradient-end'])) {
$vars['--header-gradient-end'] = 'var(--color-header-gradient-end, rgba(242, 242, 242, 1))';
}
$start = $vars['--header-gradient-start'];
$end = $vars['--header-gradient-end'];
// Keep compatibility with older CSS that expects these names.
$vars['--color-header-gradient-start'] = $start;
$vars['--color-header-gradient-end'] = $end;
$vars['--banner-gradient'] = sprintf(
'linear-gradient(90deg, %s 0, %s 90%%)',
'linear-gradient(90deg, %s 0, %s var(--banner-gradient-stop, 30%%))',
$start,
$end
);
Expand Down
Loading
Loading