Repository: authelia/authelia (go)

Issue:
Amoled dark theme
## Feature Request
Adding an amoled dark theme or a "True dark" them with darker colors than the current dark theme. (e.g. a color scheme like the github dark theme)

Or add an option to apply custom css when it comes to colors. Thank you!

Pull request description:
feat: add new oled theme
This PR introduces a new OLED theme option that should provide a true black background.

See example screenshots below.
![image](https://github.com/user-attachments/assets/f4217223-f2bc-4474-a3d9-5970e372ee32)
![image](https://github.com/user-attachments/assets/3e22764a-0ffa-4688-8c67-62063782267c)

Closes #1935 

Patch:
diff --git a/internal/configuration/schema/configuration.go b/internal/configuration/schema/configuration.go
index 607dbf64b..25cd85c1a 100644
--- a/internal/configuration/schema/configuration.go
+++ b/internal/configuration/schema/configuration.go
@@ -6,7 +6,7 @@ import (
 
 // Configuration object extracted from YAML configuration file.
 type Configuration struct {
-	Theme                 string `koanf:"theme" json:"theme" jsonschema:"default=light,enum=auto,enum=light,enum=dark,enum=grey,title=Theme Name" jsonschema_description:"The name of the theme to apply to the web UI."`
+	Theme                 string `koanf:"theme" json:"theme" jsonschema:"default=light,enum=auto,enum=light,enum=dark,enum=grey,enum=oled,title=Theme Name" jsonschema_description:"The name of the theme to apply to the web UI."`
 	CertificatesDirectory string `koanf:"certificates_directory" json:"certificates_directory" jsonschema:"title=Certificates Directory Path" jsonschema_description:"The path to a directory which is used to determine the certificates that are trusted."`
 	Default2FAMethod      string `koanf:"default_2fa_method" json:"default_2fa_method" jsonschema:"enum=totp,enum=webauthn,enum=mobile_push,title=Default 2FA method" jsonschema_description:"When a user logs in for the first time this is the 2FA method configured for them."`
 
diff --git a/internal/configuration/validator/const.go b/internal/configuration/validator/const.go
index 588c9d30b..7d3340d68 100644
--- a/internal/configuration/validator/const.go
+++ b/internal/configuration/validator/const.go
@@ -519,7 +519,7 @@ var (
 
 var (
 	validStoragePostgreSQLSSLModes           = []string{"disable", "require", "verify-ca", "verify-full"}
-	validThemeNames                          = []string{"light", "dark", "grey", auto}
+	validThemeNames                          = []string{"light", "dark", "grey", "oled", auto}
 	validSessionSameSiteValues               = []string{"none", "lax", "strict"}
 	validLogLevels                           = []string{logging.LevelTrace, logging.LevelDebug, logging.LevelInfo, logging.LevelWarn, logging.LevelError}
 	validLogFormats                          = []string{logging.FormatText, logging.FormatJSON}
diff --git a/web/src/contexts/ThemeContext.tsx b/web/src/contexts/ThemeContext.tsx
index 5841a6b6a..862ded0c2 100644
--- a/web/src/contexts/ThemeContext.tsx
+++ b/web/src/contexts/ThemeContext.tsx
@@ -128,6 +128,8 @@ function ThemeFromName(name: string) {
             return themes.Dark;
         case themes.ThemeNameGrey:
             return themes.Grey;
+        case themes.ThemeNameOled:
+            return themes.Oled;
         case themes.ThemeNameAuto:
             return window.matchMedia(MediaQueryDarkMode).matches ? themes.Dark : themes.Light;
         default:
diff --git a/web/src/themes/Oled.ts b/web/src/themes/Oled.ts
new file mode 100644
index 000000000..6fec57b2b
--- /dev/null
+++ b/web/src/themes/Oled.ts
@@ -0,0 +1,87 @@
+import { createTheme } from "@mui/material/styles";
+
+const Oled = createTheme({
+    custom: {
+        icon: "#fff",
+        loadingBar: "#fff",
+    },
+    palette: {
+        mode: "dark",
+        primary: {
+            main: "#1976d2",
+        },
+        secondary: {
+            light: "#ff4081",
+            main: "#f50057",
+            dark: "#c51162",
+            contrastText: "#ffffff",
+        },
+        error: {
+            light: "#e57373",
+            main: "#f44336",
+            dark: "#d32f2f",
+            contrastText: "#ffffff",
+        },
+        warning: {
+            light: "#ffb74d",
+            main: "#ff9800",
+            dark: "#f57c00",
+            contrastText: "rgba(255, 255, 255, 0.87)",
+        },
+        info: {
+            light: "#64b5f6",
+            main: "#2196f3",
+            dark: "#1976d2",
+            contrastText: "#ffffff",
+        },
+        success: {
+            light: "#81c784",
+            main: "#4caf50",
+            dark: "#388e3c",
+            contrastText: "rgba(255, 255, 255, 0.87)",
+        },
+        grey: {
+            "50": "#fafafa",
+            "100": "#f5f5f5",
+            "200": "#eeeeee",
+            "300": "#e0e0e0",
+            "400": "#bdbdbd",
+            "500": "#9e9e9e",
+            "600": "#757575",
+            "700": "#616161",
+            "800": "#424242",
+            "900": "#212121",
+            A100: "#d5d5d5",
+            A200: "#aaaaaa",
+            A400: "#303030",
+            A700: "#616161",
+        },
+        contrastThreshold: 3,
+        tonalOffset: 0.2,
+        text: {
+            primary: "#ffffff",
+            secondary: "rgba(255, 255, 255, 0.7)",
+            disabled: "rgba(255, 255, 255, 0.5)",
+        },
+        divider: "rgba(255, 255, 255, 0.12)",
+        background: {
+            paper: "#000000",
+            default: "#000000",
+        },
+        action: {
+            active: "#ffffff",
+            hover: "rgba(255, 255, 255, 0.08)",
+            hoverOpacity: 0.08,
+            selected: "rgba(255, 255, 255, 0.16)",
+            selectedOpacity: 0.16,
+            disabled: "rgba(255, 255, 255, 0.3)",
+            disabledBackground: "rgba(255, 255, 255, 0.12)",
+            disabledOpacity: 0.38,
+            focus: "rgba(255, 255, 255, 0.12)",
+            focusOpacity: 0.12,
+            activatedOpacity: 0.24,
+        },
+    },
+});
+
+export default Oled;
diff --git a/web/src/themes/index.ts b/web/src/themes/index.ts
index d44ee18f0..4aa8644a9 100644
--- a/web/src/themes/index.ts
+++ b/web/src/themes/index.ts
@@ -27,7 +27,9 @@ export const ThemeNameAuto = "auto";
 export const ThemeNameLight = "light";
 export const ThemeNameDark = "dark";
 export const ThemeNameGrey = "grey";
+export const ThemeNameOled = "oled";
 
 export { default as Light } from "@themes/Light";
 export { default as Dark } from "@themes/Dark";
 export { default as Grey } from "@themes/Grey";
+export { default as Oled } from "@themes/Oled";


Test patch:
diff --git a/internal/configuration/validator/theme_test.go b/internal/configuration/validator/theme_test.go
index b1c8b89bc..95cb355f9 100644
--- a/internal/configuration/validator/theme_test.go
+++ b/internal/configuration/validator/theme_test.go
@@ -36,7 +36,7 @@ func (suite *Theme) TestShouldRaiseErrorWhenInvalidThemeProvided() {
 	suite.Assert().Len(suite.validator.Warnings(), 0)
 	suite.Require().Len(suite.validator.Errors(), 1)
 
-	suite.Assert().EqualError(suite.validator.Errors()[0], "option 'theme' must be one of 'light', 'dark', 'grey', or 'auto' but it's configured as 'invalid'")
+	suite.Assert().EqualError(suite.validator.Errors()[0], "option 'theme' must be one of 'light', 'dark', 'grey', 'oled', or 'auto' but it's configured as 'invalid'")
 }
 
 func TestThemes(t *testing.T) {