mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-04-02 13:45:10 -05:00
Merge pull request #717 from paperless-ngx/fix-custom-theme-color-contrast
Fix custom theme color contrast
This commit is contained in:
commit
b4570545ef
@ -1,4 +1,3 @@
|
||||
@import "/src/theme";
|
||||
/*
|
||||
* Sidebar
|
||||
*/
|
||||
@ -36,10 +35,15 @@
|
||||
.sidebar .nav-link {
|
||||
font-weight: 500;
|
||||
|
||||
&:hover, &.active {
|
||||
&:hover, &.active, &:focus {
|
||||
color: var(--bs-primary);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
background-color: var(--bs-body-bg);
|
||||
}
|
||||
|
||||
&.active {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
@ -1,5 +1,3 @@
|
||||
@import "/src/theme";
|
||||
|
||||
.badge-corner {
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
|
@ -1,5 +1,3 @@
|
||||
@import "/src/theme";
|
||||
|
||||
form {
|
||||
position: relative;
|
||||
}
|
||||
|
@ -1,5 +1,3 @@
|
||||
@import "/src/theme";
|
||||
|
||||
.result-content {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
@ -1,5 +1,3 @@
|
||||
@import "/src/theme";
|
||||
|
||||
.card-text {
|
||||
font-size: 90%;
|
||||
}
|
||||
|
@ -1,5 +1,3 @@
|
||||
@import "/src/theme";
|
||||
|
||||
::ng-deep app-document-list app-page-header > div.mb-3 {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
@ -9,7 +9,11 @@ import {
|
||||
} from '@angular/core'
|
||||
import { Meta } from '@angular/platform-browser'
|
||||
import { CookieService } from 'ngx-cookie-service'
|
||||
import { hexToHsl } from 'src/app/utils/color'
|
||||
import {
|
||||
BRIGHTNESS,
|
||||
estimateBrightnessForColor,
|
||||
hexToHsl,
|
||||
} from 'src/app/utils/color'
|
||||
|
||||
export interface PaperlessSettings {
|
||||
key: string
|
||||
@ -132,8 +136,21 @@ export class SettingsService {
|
||||
: this.renderer.removeClass(this.document.body, 'color-scheme-dark')
|
||||
}
|
||||
|
||||
// remove these in case they were there
|
||||
this.renderer.removeClass(this.document.body, 'primary-dark')
|
||||
this.renderer.removeClass(this.document.body, 'primary-light')
|
||||
|
||||
if (themeColor) {
|
||||
const hsl = hexToHsl(themeColor)
|
||||
const bgBrightnessEstimate = estimateBrightnessForColor(themeColor)
|
||||
|
||||
if (bgBrightnessEstimate == BRIGHTNESS.DARK) {
|
||||
this.renderer.addClass(this.document.body, 'primary-dark')
|
||||
this.renderer.removeClass(this.document.body, 'primary-light')
|
||||
} else {
|
||||
this.renderer.addClass(this.document.body, 'primary-light')
|
||||
this.renderer.removeClass(this.document.body, 'primary-dark')
|
||||
}
|
||||
this.renderer.setStyle(
|
||||
document.documentElement,
|
||||
'--pngx-primary',
|
||||
|
@ -1,4 +1,9 @@
|
||||
import { HSL } from 'ngx-color'
|
||||
import { HSL, RGB } from 'ngx-color'
|
||||
|
||||
export const BRIGHTNESS = {
|
||||
LIGHT: 'light',
|
||||
DARK: 'dark',
|
||||
}
|
||||
|
||||
function componentToHex(c) {
|
||||
var hex = Math.floor(c).toString(16)
|
||||
@ -86,14 +91,42 @@ export function rgbToHsl(r, g, b) {
|
||||
}
|
||||
|
||||
export function hexToHsl(hex: string): HSL {
|
||||
const rgb = hexToRGB(hex)
|
||||
const hsl = rgbToHsl(rgb.r, rgb.g, rgb.b)
|
||||
return { h: hsl[0], s: hsl[1], l: hsl[2] }
|
||||
}
|
||||
|
||||
export function hexToRGB(hex: string): RGB {
|
||||
hex = hex.replace('#', '')
|
||||
let aRgbHex = hex.match(/.{1,2}/g)
|
||||
const hsl = rgbToHsl(
|
||||
parseInt(aRgbHex[0], 16),
|
||||
parseInt(aRgbHex[1], 16),
|
||||
parseInt(aRgbHex[2], 16)
|
||||
)
|
||||
return { h: hsl[0], s: hsl[1], l: hsl[2] }
|
||||
return {
|
||||
r: parseInt(aRgbHex[0], 16),
|
||||
g: parseInt(aRgbHex[1], 16),
|
||||
b: parseInt(aRgbHex[2], 16),
|
||||
}
|
||||
}
|
||||
|
||||
export function computeLuminance(color: RGB) {
|
||||
// Formula: http://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef
|
||||
const colorKeys = Object.keys(color)
|
||||
for (var i = 0; i < 3; i++) {
|
||||
var rgb = color[colorKeys[i]]
|
||||
rgb /= 255
|
||||
rgb = rgb < 0.03928 ? rgb / 12.92 : Math.pow((rgb + 0.055) / 1.055, 2.4)
|
||||
color[i] = rgb
|
||||
}
|
||||
return 0.2126 * color[0] + 0.7152 * color[1] + 0.0722 * color[2]
|
||||
}
|
||||
|
||||
export function estimateBrightnessForColor(colorHex: string) {
|
||||
// See <https://www.w3.org/TR/WCAG20/#contrast-ratiodef>
|
||||
// Adapted from https://api.flutter.dev/flutter/material/ThemeData/estimateBrightnessForColor.html
|
||||
const rgb = hexToRGB(colorHex)
|
||||
const luminance = computeLuminance(rgb)
|
||||
const kThreshold = 0.15
|
||||
return (luminance + 0.05) * (luminance + 0.05) > kThreshold
|
||||
? BRIGHTNESS.LIGHT
|
||||
: BRIGHTNESS.DARK
|
||||
}
|
||||
|
||||
export function randomColor() {
|
||||
|
@ -4,7 +4,6 @@ $enable-negative-margins: true;
|
||||
@import "node_modules/bootstrap/scss/bootstrap";
|
||||
@import "~@ng-select/ng-select/themes/default.theme.css";
|
||||
@import "theme";
|
||||
@import "theme_dark";
|
||||
@import "print";
|
||||
|
||||
// Paperless-ngx styles
|
||||
@ -26,6 +25,27 @@ svg.logo {
|
||||
}
|
||||
}
|
||||
|
||||
.navbar.bg-primary {
|
||||
--bs-primary: hsl(var(--pngx-primary),var(--pngx-primary-lightness));
|
||||
--bs-primary-rgb: var(--bs-primary);
|
||||
}
|
||||
|
||||
.border {
|
||||
border-color: var(--bs-border-color) !important;
|
||||
}
|
||||
|
||||
.border-end {
|
||||
border-right: 1px solid var(--bs-border-color) !important;
|
||||
}
|
||||
|
||||
.border-start {
|
||||
border-left: 1px solid var(--bs-border-color) !important;
|
||||
}
|
||||
|
||||
.border-bottom {
|
||||
border-bottom: 1px solid var(--bs-border-color) !important;
|
||||
}
|
||||
|
||||
.nav-link, .list-group-item {
|
||||
color: var(--bs-body-color);
|
||||
}
|
||||
@ -36,9 +56,19 @@ svg.logo {
|
||||
|
||||
.bg-primary {
|
||||
background-color: var(--bs-primary) !important;
|
||||
color: var(--pngx-primary-text-contrast);
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
color: var(--pngx-primary-text-contrast) !important;
|
||||
}
|
||||
|
||||
.navbar .dropdown .btn {
|
||||
color: var(--pngx-primary-text-contrast) !important;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
color: var(--pngx-primary-text-contrast);
|
||||
background-color: var(--bs-primary);
|
||||
border-color: var(--bs-primary);
|
||||
|
||||
@ -48,6 +78,7 @@ svg.logo {
|
||||
}
|
||||
|
||||
&:disabled, &.disabled {
|
||||
color: var(--pngx-primary-text-contrast);
|
||||
background-color: var(--pngx-primary-darken-10) !important;
|
||||
border-color: var(--pngx-primary-darken-10) !important;
|
||||
}
|
||||
@ -90,9 +121,9 @@ svg.logo {
|
||||
background-image: escape-svg(url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'><circle r='3' fill='#bbb'/></svg>"));
|
||||
}
|
||||
|
||||
.nav-link:focus-visible, .nav-item a:focus-visible {
|
||||
.nav-item a:focus-visible {
|
||||
outline: none;
|
||||
background-color: var(--pngx-bg-darker);
|
||||
background-color: var(--bs-body-bg);
|
||||
}
|
||||
|
||||
a.navbar-brand:focus-visible {
|
||||
@ -325,6 +356,13 @@ textarea,
|
||||
}
|
||||
}
|
||||
|
||||
.doc-img-container {
|
||||
border: none !important;
|
||||
border-top-left-radius: .25rem;
|
||||
border-top-right-radius: .25rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
// icons
|
||||
.toolbaricon {
|
||||
width: 1.2em;
|
||||
|
@ -15,3 +15,169 @@
|
||||
--pngx-bg-darker: var(--bs-gray-100);
|
||||
--pngx-focus-alpha: 0.3;
|
||||
}
|
||||
|
||||
// Dark text colors allow for maintain contrast with theme color changes
|
||||
$text-color-light-bg: #212529;
|
||||
$text-color-dark-bg: #abb2bf;
|
||||
$text-color-dark-bg-accent: lighten($text-color-dark-bg, 10%);
|
||||
// Taken from bootstrap
|
||||
$form-check-input-checked-bg-image-dark: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'><path fill='none' stroke='#212529' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10l3 3l6-6'/></svg>");
|
||||
$form-check-radio-checked-bg-image-dark: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'><circle r='2' fill='#212529'/></svg>");
|
||||
|
||||
.primary-light {
|
||||
--pngx-primary-text-contrast: #{$text-color-light-bg} !important;
|
||||
|
||||
.form-check-input:checked[type=checkbox] {
|
||||
background-image: escape-svg($form-check-input-checked-bg-image-dark);
|
||||
}
|
||||
|
||||
.form-check-input:checked[type=radio] {
|
||||
background-image: escape-svg($form-check-radio-checked-bg-image-dark);
|
||||
}
|
||||
}
|
||||
|
||||
.primary-dark {
|
||||
--pngx-primary-text-contrast: #{$text-color-dark-bg} !important;
|
||||
}
|
||||
|
||||
// Dark mode
|
||||
$primary-dark-mode: #45973a;
|
||||
$primary-dark-mode-rgb: 69, 151, 58;
|
||||
$primary-dark-mode-darken-10: darken($primary-dark-mode, 10%);
|
||||
$danger-dark-mode: #b71631;
|
||||
$danger-dark-mode-rgb: 183, 22, 49;
|
||||
$bg-dark-mode: #161618;
|
||||
$bg-dark-mode-rgb: 22, 22, 24;
|
||||
$bg-dark-mode-accent: #101216;
|
||||
$bg-dark-mode-alt: #242529;
|
||||
$bg-light-dark-mode: #1c1c1f;
|
||||
$bg-light-dark-mode-rgb: 28, 28, 31;
|
||||
$border-color-dark-mode: #47494f;
|
||||
|
||||
@mixin dark-mode {
|
||||
--bs-primary: hsl(var(--pngx-primary), calc(var(--pngx-primary-lightness) + 10%));
|
||||
--bs-body-color: #{$text-color-dark-bg};
|
||||
--pngx-body-color-accent: #{$text-color-dark-bg-accent};
|
||||
--bs-danger: #{$danger-dark-mode};
|
||||
--bs-danger-rgb: #{$danger-dark-mode-rgb};
|
||||
--bs-body-bg: #{$bg-dark-mode};
|
||||
--bs-body-bg-rgb: #{$bg-dark-mode-rgb};
|
||||
--bs-light: #{$bg-light-dark-mode};
|
||||
--bs-light-rgb: #{$bg-light-dark-mode-rgb};
|
||||
--bs-border-color: #{$border-color-dark-mode};
|
||||
--pngx-bg-darker: #{$bg-dark-mode-accent};
|
||||
--pngx-bg-alt: #{$bg-dark-mode-alt};
|
||||
--pngx-focus-alpha: 0.7;
|
||||
--pngx-primary-faded: var(--pngx-primary-darken-15);
|
||||
--pngx-primary-text-contrast: var(--bs-body-color);
|
||||
|
||||
.text-dark, .text-light {
|
||||
color: var(--bs-body-color) !important;
|
||||
}
|
||||
|
||||
.btn-outline-primary, .btn-primary {
|
||||
&:hover, &:focus, &.active, &:active {
|
||||
color: var(--bs-light) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-outline-secondary {
|
||||
&:hover, &:focus, &.active, &:active {
|
||||
background-color: var(--pngx-bg-darker);
|
||||
color: var(--bs-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.search-form-container {
|
||||
input, input:focus {
|
||||
color: var(--bs-body-color) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
background-color: var(--bs-body-bg);
|
||||
|
||||
.card-header {
|
||||
background-color: rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
}
|
||||
|
||||
.modal-content, .modal-header, .modal-body, .modal-footer {
|
||||
background-color: var(--bs-body-bg);
|
||||
border-color: var(--bs-border-color);
|
||||
}
|
||||
|
||||
app-tag .badge {
|
||||
filter: brightness(.8);
|
||||
}
|
||||
|
||||
.doc-img {
|
||||
mix-blend-mode: normal;
|
||||
border-radius: 0;
|
||||
border-color: var(--bs-border-color);
|
||||
filter: invert(10%);
|
||||
|
||||
&.border-end {
|
||||
border-right: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.doc-img.inverted {
|
||||
filter: invert(95%) hue-rotate(180deg);
|
||||
}
|
||||
|
||||
.card-selected .doc-img {
|
||||
mix-blend-mode: luminosity;
|
||||
}
|
||||
|
||||
.ng-dropdown-panel .ng-dropdown-panel-items .ng-option:hover,
|
||||
.ng-dropdown-panel .ng-dropdown-panel-items .ng-option.ng-option-marked {
|
||||
background-color: $bg-light-dark-mode;
|
||||
}
|
||||
|
||||
table {
|
||||
.des,
|
||||
.asc {
|
||||
&::after {
|
||||
filter: invert(0.8); /* arrow is a black inline png bkgd image (!) so use filter */
|
||||
}
|
||||
}
|
||||
|
||||
&.table-hover > tbody > tr:hover > * {
|
||||
background-color: $bg-light-dark-mode;
|
||||
color: var(--pngx-body-color-accent);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.table-striped > tbody > tr:nth-of-type(odd) > * {
|
||||
color: var(--pngx-body-color-accent);
|
||||
}
|
||||
|
||||
.close, .modal .btn-close, .alert .btn-close {
|
||||
text-shadow: 0 1px 0 #666;
|
||||
}
|
||||
|
||||
.modal .btn-close, .alert .btn-close {
|
||||
filter: invert(1) grayscale(100%) brightness(200%);
|
||||
}
|
||||
|
||||
.toast, .toast-header {
|
||||
background-color: hsla(var(--pngx-primary), calc(var(--pngx-primary-lightness) - 15%), 0.9);
|
||||
}
|
||||
|
||||
.toast,
|
||||
.toast .toast-header,
|
||||
.toast .btn-close {
|
||||
color: var(--pngx-primary-text-contrast);
|
||||
}
|
||||
}
|
||||
|
||||
body.color-scheme-dark {
|
||||
@include dark-mode;
|
||||
}
|
||||
body.color-scheme-system {
|
||||
@media (prefers-color-scheme: dark) {
|
||||
@include dark-mode;
|
||||
}
|
||||
}
|
||||
|
@ -1,171 +0,0 @@
|
||||
$primary-dark-mode: #45973a;
|
||||
$primary-dark-mode-rgb: 69, 151, 58;
|
||||
$primary-dark-mode-darken-10: darken($primary-dark-mode, 10%);
|
||||
$danger-dark-mode: #b71631;
|
||||
$danger-dark-mode-rgb: 183, 22, 49;
|
||||
$bg-dark-mode: #161618;
|
||||
$bg-dark-mode-rgb: 22, 22, 24;
|
||||
$bg-dark-mode-accent: #101216;
|
||||
$bg-dark-mode-alt: #242529;
|
||||
$bg-light-dark-mode: #1c1c1f;
|
||||
$bg-light-dark-mode-rgb: 28, 28, 31;
|
||||
$text-color-dark-mode: #abb2bf;
|
||||
$text-color-dark-mode-accent: lighten($text-color-dark-mode, 10%);
|
||||
$border-color-dark-mode: #47494f;
|
||||
|
||||
@mixin dark-mode {
|
||||
--bs-primary: hsl(var(--pngx-primary), calc(var(--pngx-primary-lightness) + 10%));
|
||||
--bs-danger: #{$danger-dark-mode};
|
||||
--bs-danger-rgb: #{$danger-dark-mode-rgb};
|
||||
--bs-body-bg: #{$bg-dark-mode};
|
||||
--bs-body-bg-rgb: #{$bg-dark-mode-rgb};
|
||||
--bs-body-color: #{$text-color-dark-mode};
|
||||
--bs-light: #{$bg-light-dark-mode};
|
||||
--bs-light-rgb: #{$bg-light-dark-mode-rgb};
|
||||
--bs-border-color: #{$border-color-dark-mode};
|
||||
--pngx-bg-darker: #{$bg-dark-mode-accent};
|
||||
--pngx-bg-alt: #{$bg-dark-mode-alt};
|
||||
--pngx-body-color-accent: #{$text-color-dark-mode-accent};
|
||||
--pngx-focus-alpha: 0.7;
|
||||
--pngx-primary-faded: var(--pngx-primary-darken-15);
|
||||
--pngx-primary-text-contrast: var(--bs-body-color);
|
||||
|
||||
.navbar.bg-primary{
|
||||
--bs-primary: hsl(var(--pngx-primary),var(--pngx-primary-lightness));
|
||||
--bs-primary-rgb: var(--bs-primary);
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
color: var(--bs-body-color);
|
||||
}
|
||||
|
||||
.border {
|
||||
border-color: var(--bs-border-color) !important;
|
||||
}
|
||||
|
||||
.border-end {
|
||||
border-right: 1px solid var(--bs-border-color) !important;
|
||||
}
|
||||
|
||||
.border-start {
|
||||
border-left: 1px solid var(--bs-border-color) !important;
|
||||
}
|
||||
|
||||
.border-bottom {
|
||||
border-bottom: 1px solid var(--bs-border-color) !important;
|
||||
}
|
||||
|
||||
.text-dark, .text-light {
|
||||
color: var(--bs-body-color) !important;
|
||||
}
|
||||
|
||||
.btn-outline-primary, .btn-primary {
|
||||
&:hover, &:focus, &.active, &:active {
|
||||
color: var(--bs-light) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-outline-secondary {
|
||||
&:hover, &:focus, &.active, &:active {
|
||||
background-color: var(--pngx-bg-darker);
|
||||
color: var(--bs-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.search-form-container {
|
||||
input, input:focus {
|
||||
color: var(--bs-body-color) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
background-color: var(--bs-body-bg);
|
||||
|
||||
.card-header {
|
||||
background-color: rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
}
|
||||
|
||||
.modal-content, .modal-header, .modal-body, .modal-footer {
|
||||
background-color: var(--bs-body-bg);
|
||||
border-color: var(--bs-border-color);
|
||||
}
|
||||
|
||||
app-tag .badge {
|
||||
filter: brightness(.8);
|
||||
}
|
||||
|
||||
.doc-img-container {
|
||||
border: none !important;
|
||||
border-top-left-radius: .25rem;
|
||||
border-top-right-radius: .25rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.doc-img {
|
||||
mix-blend-mode: normal;
|
||||
border-radius: 0;
|
||||
border-color: var(--bs-border-color);
|
||||
filter: invert(10%);
|
||||
|
||||
&.border-end {
|
||||
border-right: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.doc-img.inverted {
|
||||
filter: invert(95%) hue-rotate(180deg);
|
||||
}
|
||||
|
||||
.card-selected .doc-img {
|
||||
mix-blend-mode: luminosity;
|
||||
}
|
||||
|
||||
.ng-dropdown-panel .ng-dropdown-panel-items .ng-option:hover,
|
||||
.ng-dropdown-panel .ng-dropdown-panel-items .ng-option.ng-option-marked {
|
||||
background-color: $bg-light-dark-mode;
|
||||
}
|
||||
|
||||
table {
|
||||
.des,
|
||||
.asc {
|
||||
&::after {
|
||||
filter: invert(0.8); /* arrow is a black inline png bkgd image (!) so use filter */
|
||||
}
|
||||
}
|
||||
|
||||
&.table-hover > tbody > tr:hover > * {
|
||||
background-color: $bg-light-dark-mode;
|
||||
color: $text-color-dark-mode-accent;
|
||||
}
|
||||
}
|
||||
|
||||
.table-striped > tbody > tr:nth-of-type(odd) > * {
|
||||
color: $text-color-dark-mode-accent;
|
||||
}
|
||||
|
||||
.close, .modal .btn-close, .alert .btn-close {
|
||||
text-shadow: 0 1px 0 #666;
|
||||
}
|
||||
|
||||
.modal .btn-close, .alert .btn-close {
|
||||
filter: invert(1) grayscale(100%) brightness(200%);
|
||||
}
|
||||
|
||||
.toast {
|
||||
background-color: hsla(var(--pngx-primary), calc(var(--pngx-primary-lightness) - 18%), 0.9);
|
||||
}
|
||||
|
||||
.toast-header {
|
||||
background-color: hsla(var(--pngx-primary), calc(var(--pngx-primary-lightness) - 10%), 0.9);
|
||||
}
|
||||
}
|
||||
|
||||
body.color-scheme-dark {
|
||||
@include dark-mode;
|
||||
}
|
||||
body.color-scheme-system {
|
||||
@media (prefers-color-scheme: dark) {
|
||||
@include dark-mode;
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user