Merge pull request #243 from paperless-ngx/css-variables

Custom color theming
This commit is contained in:
shamoon 2022-03-10 19:26:15 -08:00 committed by GitHub
commit 8f1c4cd9c4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 473 additions and 385 deletions

View File

@ -19,7 +19,7 @@ export class AppComponent implements OnInit, OnDestroy {
constructor (private settings: SettingsService, private consumerStatusService: ConsumerStatusService, private toastService: ToastService, private router: Router) {
let anyWindow = (window as any)
anyWindow.pdfWorkerSrc = 'assets/js/pdf.worker.min.js';
this.settings.updateDarkModeSettings()
this.settings.updateAppearanceSettings()
}
ngOnDestroy(): void {

View File

@ -92,7 +92,7 @@
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#file-text"/>
</svg>&nbsp;{{d.title | documentTitle}}
<span class="close bg-light" (click)="closeDocument(d); $event.preventDefault()">
<span class="close" (click)="closeDocument(d); $event.preventDefault()">
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="bi bi-x" viewBox="0 0 16 16">
<use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg>
@ -169,7 +169,7 @@
</li>
<li class="nav-item">
<div class="d-flex w-100 flex-wrap">
<a class="nav-link pe-0 pb-0" target="_blank" rel="noopener noreferrer" href="https://github.com/paperless-ngx/paperless-ngx">
<a class="nav-link pe-2 pb-1" target="_blank" rel="noopener noreferrer" href="https://github.com/paperless-ngx/paperless-ngx">
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="sidebaricon bi bi-github" viewBox="0 0 16 16">
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z"/>
</svg>&nbsp;<ng-container i18n>GitHub</ng-container>

View File

@ -35,16 +35,14 @@
.sidebar .nav-link {
font-weight: 500;
color: #333;
}
.sidebar .nav-link .sidebaricon {
margin-right: 4px;
color: #999;
}
.sidebar .nav-link.active {
color: $primary;
color: var(--bs-primary);
font-weight: bold;
}
@ -172,8 +170,7 @@
}
&:focus {
background-color: #fff;
color: #212529;
background-color: rgba(0, 0, 0, 0.3);
flex-grow: 1;
padding-left: 0.5rem;
}

View File

@ -8,7 +8,7 @@
<p *ngIf="message">{{message}}</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-dark" (click)="cancel()" [disabled]="!buttonsEnabled" i18n>Cancel</button>
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" [disabled]="!buttonsEnabled" i18n>Cancel</button>
<button type="button" class="btn" [class]="btnClass" (click)="confirm()" [disabled]="!confirmButtonEnabled || !buttonsEnabled">
{{btnCaption}}
<span *ngIf="!confirmButtonEnabled"> ({{seconds}})</span>

View File

@ -42,7 +42,7 @@
filter: brightness(0.5);
&.active {
background-color: lighten($primary, 30%);
background-color: var(--ngx-primary-lighten-30);
}
}

View File

@ -1,5 +1,5 @@
<div class="mb-3">
<label [for]="inputId">{{title}}</label>
<label *ngIf="title" [for]="inputId">{{title}}</label>
<div class="input-group" [class.is-invalid]="error">
<span class="input-group-text" [style.background-color]="value">&nbsp;&nbsp;&nbsp;</span>

View File

@ -1,9 +1,9 @@
<app-widget-frame [title]="savedView.name">
<a header-buttons [routerLink]="[]" (click)="showAll()" i18n>Show all</a>
<a class="btn-link" header-buttons [routerLink]="[]" (click)="showAll()" i18n>Show all</a>
<table content class="table table-sm bg-light table-hover table-borderless mb-0">
<table content class="table table-sm table-hover table-borderless mb-0">
<thead>
<tr>
<th i18n>Created</th>

View File

@ -6,7 +6,7 @@
</div>
</div>
<div class="card-body text-dark">
<div class="card-body text-dark bg-light">
<ng-content select ="[content]"></ng-content>
</div>
</div>

View File

@ -17,7 +17,7 @@
<div class="d-flex justify-content-between align-items-center">
<h5 class="card-title">
<ng-container *ngIf="document.correspondent">
<a *ngIf="clickCorrespondent.observers.length ; else nolink" [routerLink]="[]" title="Filter by correspondent" i18n-title (click)="clickCorrespondent.emit(document.correspondent);$event.stopPropagation()" class="fw-bold">{{(document.correspondent$ | async)?.name}}</a>
<a *ngIf="clickCorrespondent.observers.length ; else nolink" [routerLink]="[]" title="Filter by correspondent" i18n-title (click)="clickCorrespondent.emit(document.correspondent);$event.stopPropagation()" class="fw-bold btn-link">{{(document.correspondent$ | async)?.name}}</a>
<ng-template #nolink>{{(document.correspondent$ | async)?.name}}</ng-template>:
</ng-container>
{{document.title | documentTitle}}

View File

@ -52,7 +52,7 @@
}
.card-selected {
border-color: $primary;
border-color: var(--bs-primary);
.document-card-check {
display: block;
@ -60,7 +60,7 @@
}
.doc-img-background-selected {
background-color: $primaryFaded;
background-color: var(--ngx-primary-faded);
}
.card-info {

View File

@ -23,7 +23,7 @@
<div class="card-body p-2">
<p class="card-text">
<ng-container *ngIf="document.correspondent">
<a [routerLink]="[]" title="Filter by correspondent" i18n-title (click)="clickCorrespondent.emit(document.correspondent);$event.stopPropagation()" class="fw-bold">{{(document.correspondent$ | async)?.name}}</a>:
<a [routerLink]="[]" title="Filter by correspondent" i18n-title (click)="clickCorrespondent.emit(document.correspondent);$event.stopPropagation()" class="fw-bold btn-link">{{(document.correspondent$ | async)?.name}}</a>:
</ng-container>
{{document.title | documentTitle}}
</p>

View File

@ -37,7 +37,7 @@
}
.card-selected {
border-color: $primary;
border-color:var(--bs-primary);
.document-card-check {
display: block;
@ -45,7 +45,7 @@
}
.doc-img-background-selected {
background-color: $primaryFaded;
background-color: var(--ngx-primary-faded);
}
.card-info {
@ -57,7 +57,7 @@
&:hover,
&:focus {
background-color: transparent !important;
color: $primary;
color: var(--bs-primary);
}
}

View File

@ -5,7 +5,7 @@ tr {
}
.table-row-selected {
background-color: $primaryFaded;
background-color: var(--ngx-primary-faded);
}
$paperless-card-breakpoints: (

View File

@ -11,7 +11,7 @@
<app-input-check *ngIf="patternRequired" i18n-title title="Case insensitive" formControlName="is_insensitive" novalidate></app-input-check>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-dark" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>
<button type="submit" class="btn btn-primary" i18n [disabled]="networkActive">Save</button>
</div>
</form>

View File

@ -13,7 +13,7 @@
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-dark" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>
<button type="submit" class="btn btn-primary" i18n [disabled]="networkActive">Save</button>
</div>
</form>

View File

@ -100,6 +100,22 @@
</div>
</div>
<div class="row mb-3">
<div class="col-md-3 col-form-label">
<span i18n>Theme Color</span>
</div>
<div class="col-3">
<app-input-color i18n-title formControlName="themeColor" [error]="error?.color"></app-input-color>
</div>
<div class="col-2">
<button class="btn btn-link btn-sm pt-2 ps-0" [disabled]="!this.settingsForm.get('themeColor').value" (click)="clearThemeColor()">
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-x me-1" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/>
</svg><ng-container i18n>Reset</ng-container>
</button>
</div>
</div>
<h4 class="mt-4" i18n>Bulk editing</h4>
<div class="row mb-3">

View File

@ -24,6 +24,7 @@ export class SettingsComponent implements OnInit, OnDestroy, DirtyComponent {
'darkModeUseSystem': new FormControl(null),
'darkModeEnabled': new FormControl(null),
'darkModeInvertThumbs': new FormControl(null),
'themeColor': new FormControl(null),
'useNativePdfViewer': new FormControl(null),
'savedViews': this.savedViewGroup,
'displayLanguage': new FormControl(null),
@ -40,6 +41,7 @@ export class SettingsComponent implements OnInit, OnDestroy, DirtyComponent {
store: BehaviorSubject<any>
storeSub: Subscription
isDirty$: Observable<boolean>
isDirty: Boolean = false
get computedDateLocale(): string {
return this.settingsForm.value.dateLocale || this.settingsForm.value.displayLanguage || this.currentLocale
@ -63,6 +65,7 @@ export class SettingsComponent implements OnInit, OnDestroy, DirtyComponent {
'darkModeUseSystem': this.settings.get(SETTINGS_KEYS.DARK_MODE_USE_SYSTEM),
'darkModeEnabled': this.settings.get(SETTINGS_KEYS.DARK_MODE_ENABLED),
'darkModeInvertThumbs': this.settings.get(SETTINGS_KEYS.DARK_MODE_THUMB_INVERTED),
'themeColor': this.settings.get(SETTINGS_KEYS.THEME_COLOR),
'useNativePdfViewer': this.settings.get(SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER),
'savedViews': {},
'displayLanguage': this.settings.getLanguage(),
@ -97,10 +100,21 @@ export class SettingsComponent implements OnInit, OnDestroy, DirtyComponent {
// Initialize dirtyCheck
this.isDirty$ = dirtyCheck(this.settingsForm, this.store.asObservable())
// Record dirty in case we need to 'undo' appearance settings if not saved on close
this.isDirty$.subscribe(dirty => {
this.isDirty = dirty
})
// "Live" visual changes prior to save
this.settingsForm.valueChanges.subscribe(() => {
this.settings.updateAppearanceSettings(this.settingsForm.get('darkModeUseSystem').value, this.settingsForm.get('darkModeEnabled').value, this.settingsForm.get('themeColor').value)
})
})
}
ngOnDestroy() {
if (this.isDirty) this.settings.updateAppearanceSettings() // in case user changed appearance but didnt save
this.storeSub && this.storeSub.unsubscribe();
}
@ -119,6 +133,7 @@ export class SettingsComponent implements OnInit, OnDestroy, DirtyComponent {
this.settings.set(SETTINGS_KEYS.DARK_MODE_USE_SYSTEM, this.settingsForm.value.darkModeUseSystem)
this.settings.set(SETTINGS_KEYS.DARK_MODE_ENABLED, (this.settingsForm.value.darkModeEnabled == true).toString())
this.settings.set(SETTINGS_KEYS.DARK_MODE_THUMB_INVERTED, (this.settingsForm.value.darkModeInvertThumbs == true).toString())
this.settings.set(SETTINGS_KEYS.THEME_COLOR, (this.settingsForm.value.themeColor).toString())
this.settings.set(SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER, this.settingsForm.value.useNativePdfViewer)
this.settings.set(SETTINGS_KEYS.DATE_LOCALE, this.settingsForm.value.dateLocale)
this.settings.set(SETTINGS_KEYS.DATE_FORMAT, this.settingsForm.value.dateFormat)
@ -129,7 +144,7 @@ export class SettingsComponent implements OnInit, OnDestroy, DirtyComponent {
this.settings.setLanguage(this.settingsForm.value.displayLanguage)
this.store.next(this.settingsForm.value)
this.documentListViewService.updatePageSize()
this.settings.updateDarkModeSettings()
this.settings.updateAppearanceSettings()
this.toastService.showInfo($localize`Settings saved successfully.`)
}
@ -165,4 +180,8 @@ export class SettingsComponent implements OnInit, OnDestroy, DirtyComponent {
}
}
clearThemeColor() {
this.settingsForm.get('themeColor').patchValue('');
}
}

View File

@ -15,7 +15,7 @@
<app-input-check *ngIf="patternRequired" i18n-title title="Case insensitive" formControlName="is_insensitive"></app-input-check>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-dark" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>
<button type="submit" class="btn btn-primary" i18n [disabled]="networkActive">Save</button>
</div>
</form>

View File

@ -1,7 +1,8 @@
import { DOCUMENT } from '@angular/common';
import { Inject, Injectable, LOCALE_ID, Renderer2, RendererFactory2 } from '@angular/core';
import { Inject, Injectable, LOCALE_ID, Renderer2, RendererFactory2, RendererStyleFlags2 } from '@angular/core';
import { Meta } from '@angular/platform-browser';
import { CookieService } from 'ngx-cookie-service';
import { hexToHsl } from 'src/app/utils/color';
export interface PaperlessSettings {
key: string
@ -27,6 +28,7 @@ export const SETTINGS_KEYS = {
DARK_MODE_USE_SYSTEM: 'general-settings:dark-mode:use-system',
DARK_MODE_ENABLED: 'general-settings:dark-mode:enabled',
DARK_MODE_THUMB_INVERTED: 'general-settings:dark-mode:thumb-inverted',
THEME_COLOR: 'general-settings:theme:color',
USE_NATIVE_PDF_VIEWER: 'general-settings:document-details:native-pdf-viewer',
DATE_LOCALE: 'general-settings:date-display:date-locale',
DATE_FORMAT: 'general-settings:date-display:date-format',
@ -43,6 +45,7 @@ const SETTINGS: PaperlessSettings[] = [
{key: SETTINGS_KEYS.DARK_MODE_USE_SYSTEM, type: "boolean", default: true},
{key: SETTINGS_KEYS.DARK_MODE_ENABLED, type: "boolean", default: false},
{key: SETTINGS_KEYS.DARK_MODE_THUMB_INVERTED, type: "boolean", default: true},
{key: SETTINGS_KEYS.THEME_COLOR, type: "string", default: ""},
{key: SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER, type: "boolean", default: false},
{key: SETTINGS_KEYS.DATE_LOCALE, type: "string", default: ""},
{key: SETTINGS_KEYS.DATE_FORMAT, type: "string", default: "mediumDate"},
@ -68,12 +71,13 @@ export class SettingsService {
) {
this.renderer = rendererFactory.createRenderer(null, null);
this.updateDarkModeSettings()
this.updateAppearanceSettings()
}
updateDarkModeSettings(): void {
let darkModeUseSystem = this.get(SETTINGS_KEYS.DARK_MODE_USE_SYSTEM)
let darkModeEnabled = this.get(SETTINGS_KEYS.DARK_MODE_ENABLED)
public updateAppearanceSettings(darkModeUseSystem = null, darkModeEnabled = null, themeColor = null): void {
darkModeUseSystem ??= this.get(SETTINGS_KEYS.DARK_MODE_USE_SYSTEM)
darkModeEnabled ??= this.get(SETTINGS_KEYS.DARK_MODE_ENABLED)
themeColor ??= this.get(SETTINGS_KEYS.THEME_COLOR);
if (darkModeUseSystem) {
this.renderer.addClass(this.document.body, 'color-scheme-system')
@ -83,6 +87,14 @@ export class SettingsService {
darkModeEnabled ? this.renderer.addClass(this.document.body, 'color-scheme-dark') : this.renderer.removeClass(this.document.body, 'color-scheme-dark')
}
if (themeColor) {
const hsl = hexToHsl(themeColor)
this.renderer.setStyle(document.documentElement, '--pngx-primary',`${+hsl.h * 360},${hsl.s * 100}%`, RendererStyleFlags2.DashCase)
this.renderer.setStyle(document.documentElement, '--pngx-primary-lightness',`${hsl.l * 100}%`, RendererStyleFlags2.DashCase)
} else {
this.renderer.removeStyle(document.documentElement, '--pngx-primary', RendererStyleFlags2.DashCase)
this.renderer.removeStyle(document.documentElement, '--pngx-primary-lightness', RendererStyleFlags2.DashCase)
}
}
getLanguageOptions(): LanguageOption[] {

View File

@ -1,3 +1,4 @@
import { HSL } from "ngx-color"
function componentToHex(c) {
var hex = Math.floor(c).toString(16)
@ -5,8 +6,8 @@ function componentToHex(c) {
}
/**
* https://axonflux.com/handy-rgb-to-hsl-and-rgb-to-hsv-color-model-c
*
* https://axonflux.com/handy-rgb-to-hsl-and-rgb-to-hsv-color-model-c
* Converts an HSL color value to RGB. Conversion formula
* adapted from http://en.wikipedia.org/wiki/HSL_color_space.
* Assumes h, s, and l are contained in the set [0, 1] and
@ -42,6 +43,46 @@ function hslToRgb(h, s, l){
return [r * 255, g * 255, b * 255]
}
/**
* https://axonflux.com/handy-rgb-to-hsl-and-rgb-to-hsv-color-model-c
* Converts an RGB color value to HSL. Conversion formula
* adapted from http://en.wikipedia.org/wiki/HSL_color_space.
* Assumes r, g, and b are contained in the set [0, 255] and
* returns h, s, and l in the set [0, 1].
*
* @param Number r The red color value
* @param Number g The green color value
* @param Number b The blue color value
* @return Array The HSL representation
*/
export function rgbToHsl(r, g, b){
r /= 255, g /= 255, b /= 255;
var max = Math.max(r, g, b), min = Math.min(r, g, b);
var h, s, l = (max + min) / 2;
if(max == min){
h = s = 0; // achromatic
}else{
var d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch(max){
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
case g: h = (b - r) / d + 2; break;
case b: h = (r - g) / d + 4; break;
}
h /= 6;
}
return [h, s, l];
}
export function hexToHsl(hex: string): HSL {
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] }
}
export function randomColor() {
let rgb = hslToRgb(Math.random(), 0.6, Math.random() * 0.4 + 0.4)
return `#${componentToHex(rgb[0])}${componentToHex(rgb[1])}${componentToHex(rgb[2])}`

View File

@ -1,8 +1,11 @@
// bs options
$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";
@import "node_modules/bootstrap/scss/bootstrap";
@import "~@ng-select/ng-select/themes/default.theme.css";
.toolbaricon {
width: 1.2em;
@ -113,10 +116,6 @@ body {
}
}
.ngx-file-drop__drop-zone--over {
background-color: $primaryFaded !important;
}
// Bootstrap 5 tweaks
a.badge {
text-decoration: none;
@ -130,6 +129,6 @@ a.badge {
}
.btn-outline-primary:not(:disabled):not(.disabled).active,.btn-outline-primary:not(:disabled):not(.disabled):active,.show>.btn-outline-primary.dropdown-toggle {
background-color: $paperless-green;
border-color: $paperless-green;
background-color: var(--bs-primary);
border-color: var(--bs-primary);
}

View File

@ -1,8 +1,294 @@
$paperless-green: #17541f;
$primary: #17541f;
$primaryFaded: #d1ddd2;
$enable-negative-margins: true;
:root {
// base color e.g. #17541f = hsl(128, 57%, 21%)
--pngx-primary: 128, 57%;
--pngx-primary-lightness: 21%;
--bs-primary: hsl(var(--pngx-primary), var(--pngx-primary-lightness));
--bs-border-color: var(--bs-gray-400);
--ngx-primary-faded: hsl(var(--pngx-primary), calc(var(--pngx-primary-lightness) + 72%));
--ngx-primary-lighten-10: hsl(var(--pngx-primary), calc(var(--pngx-primary-lightness) + 10%));
--ngx-primary-lighten-30: hsl(var(--pngx-primary), calc(var(--pngx-primary-lightness) + 30%));
--ngx-primary-darken-10: hsl(var(--pngx-primary), calc(var(--pngx-primary-lightness) - 10%));
--ngx-primary-darken-15: hsl(var(--pngx-primary), calc(var(--pngx-primary-lightness) - 15%));
--ngx-primary-darken-18: hsl(var(--pngx-primary), calc(var(--pngx-primary-lightness) - 18%));
--ngx-bg-darker: var(--bs-gray-100);
--ngx-focus-alpha: 0.3;
}
svg.logo {
.leaf {
fill: var(--bs-primary) !important;
}
.text {
fill: var(--bs-body-color) !important;
}
}
.nav-link, .list-group-item {
color: var(--bs-body-color);
}
.bg-body {
background-color: #fff;
background-color: var(--bs-body-bg);
}
.bg-primary {
background-color: var(--bs-primary) !important;
}
.btn-primary {
background-color: var(--bs-primary);
border-color: var(--bs-primary);
&:hover, &:focus {
background-color: var(--ngx-primary-darken-10);
border-color: var(--ngx-primary-darken-10);
}
&:disabled, &.disabled {
background-color: var(--ngx-primary-darken-10) !important;
border-color: var(--ngx-primary-darken-10) !important;
}
}
.btn-outline-primary {
border-color: var(--bs-primary) !important;
color: var(--bs-primary) !important;
&:hover, &:focus, &.active, &:active {
background-color: var(--bs-primary) !important;
color: var(--bs-light) !important;
}
}
.btn-outline-secondary {
color: var(--bs-secondary);
}
.nav-item .sidebaricon {
color: var(--bs-secondary);
}
.btn:focus,
.btn:active:focus,
.dropdown-item:focus,
.btn-check:focus + .btn,
.form-control:focus,
.form-check-input:focus,
.form-check-radio:focus,
.form-select:focus {
box-shadow: 0 0 0 0.25rem hsla(var(--pngx-primary), var(--pngx-primary-lightness), var(--ngx-focus-alpha));
}
.form-switch .form-check-input:focus {
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 {
outline: none;
background-color: var(--ngx-bg-darker);
}
a.navbar-brand:focus-visible {
outline: none;
color: var(--ngx-primary-darken-10);
}
.dropdown.show {
> .btn-primary {
background-color: var(--bs-primary);
border-color: var(--bs-primary);
}
> .btn-outline-primary {
color: var(--bs-body-color) !important;
}
}
a, a:hover, .btn-link, .btn-link:hover {
color: var(--bs-primary);
}
.form-control:not(.btn),
input,
select,
textarea,
.form-select:not(.is-invalid):not(:disabled),
.form-check-input {
color: var(--bs-body-color);
background-color: var(--bs-body-bg);
border-color: var(--bs-border-color);
&:focus {
background-color: var(--ngx-bg-darker);
color: var(--bs-body-color) !important;
}
}
.form-check-input:checked {
background-color: var(--bs-primary);
border-color: var(--bs-primary);
}
.form-check-input:focus {
border-color: var(--bs-primary);
}
.page-link {
color: var(--bs-light);
background-color: var(--bs-body-bg);
border-color: var(--bs-border-color) !important;
&:hover, &:focus {
background-color: var(--bs-primary) !important;
color: var(--bs-light) !important;
}
}
.page-item.active .page-link {
background-color: var(--bs-primary);
border-color: var(--bs-primary) !important;
color: var(--bs-light);
}
.page-item.disabled .page-link {
background-color: var(--ngx-bg-darker);
}
.nav-tabs {
border-bottom: 1px solid var(--bs-border-color);
.nav-link {
color: var(--bs-primary);
&.active, &:hover {
border-color: var(--bs-border-color);
background-color: var(--bs-body-bg);
color: var(--bs-body-color);
border-bottom: 1px solid transparent;
}
&:focus {
border-color: var(--bs-border-color);
}
&.active:focus, &:active {
border-bottom: 1px solid transparent;
}
}
}
.ng-select-container,
.ng-select.ng-select-opened > .ng-select-container,
.ng-dropdown-panel,
.ng-dropdown-panel .ng-dropdown-panel-items .ng-option {
background-color: var(--bs-body-bg) !important;
color: var(--bs-body-color) !important;
border-color: var(--bs-border-color) !important;
input:focus {
background-color: transparent !important;
}
}
.input-group-text {
color: var(--bs-body-color);
background-color: var(--bs-body-bg);
border-color: var(--bs-border-color);
}
.list-group-item {
color: var(--bs-body-color);
background-color: var(--bs-body-bg);
border-color: var(--bs-border-color);
&:hover, &:focus {
background-color: var(--bs-body-bg);
}
}
.dropdown-menu {
background-color: var(--bs-body-bg);
.dropdown-divider {
border-color: var(--bs-border-color);
}
.dropdown-item {
color: var(--bs-body-color);
&:hover {
background-color: var(--ngx-bg-darker);
color: var(--bs-body-color);
}
&.active {
background-color: var(--bs-primary);
}
}
}
table.table {
color: var(--bs-body-color);
.des,.asc {
background-color: var(--bs-body-bg) !important;
}
}
.close {
color: var(--bs-body-color);
}
.modal .btn-close {
color: var(--bs-body-color);
}
.ngx-file-drop__drop-zone--over {
background-color: var(--ngx-primary-faded) !important;
}
.alert-danger {
color: var(--bs-body-color);
background-color: var(--bs-danger);
border-color: var(--bs-danger);
}
.alert-secondary {
background-color: var(--ngx-primary-darken-18);
border-color: var(--ngx-primary-darken-15);
color: var(--bs-body-color);
}
.ngb-dp-header,
.ngb-dp-weekdays,
.ngb-dp-month {
background-color: var(--bs-body-bg);
}
.popover {
.popover-header,
.popover-body {
background-color: var(--ngx-bg-alt);
border-color: var(--bs-border-color);
}
}
// fix popover carat colors
.bs-popover-start > .popover-arrow::after, .bs-popover-auto[data-popper-placement^="left"] {
border-left-color: var(--ngx-bg-alt);
}
.bs-popover-end > .popover-arrow::after, .bs-popover-auto[data-popper-placement^="right"] {
border-right-color: var(--ngx-bg-alt);
}
.bs-popover-top > .popover-arrow::after, .bs-popover-auto[data-popper-placement^="top"] {
border-top-color: var(--ngx-bg-alt);
}
.bs-popover-bottom > .popover-arrow::after, .bs-popover-auto[data-popper-placement^="bottom"] {
border-bottom-color: var(--ngx-bg-alt);
}
.bs-popover-bottom .popover-header::before,
.bs-popover-auto[x-placement^=bottom] .popover-header::before {
border-bottom-color: var(--ngx-bg-alt);
}

View File

@ -1,8 +1,14 @@
$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-accent: #21262d;
$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;
@ -12,141 +18,79 @@ $border-color-dark-mode: #47494f;
}
@mixin dark-mode {
background-color: $bg-dark-mode !important;
color: $text-color-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};
--ngx-bg-darker: #{$bg-dark-mode-accent};
--ngx-bg-alt: #{$bg-dark-mode-alt};
--ngx-body-color-accent: #{$text-color-dark-mode-accent};
--ngx-focus-alpha: 0.7;
.navbar.bg-primary{
--bs-primary: hsl(var(--pngx-primary),var(--pngx-primary-lightness));
--bs-primary-rgb: var(--bs-primary);
}
.navbar-brand {
color: $text-color-dark-mode;
color: var(--bs-body-color);
}
svg.logo {
.leaf {
color: $primary-dark-mode !important;
}
.text {
fill: $text-color-dark-mode !important;
}
}
.bg-light {
background-color: $bg-light-dark-mode !important;
a,
div {
color: $text-color-dark-mode;
}
}
.bg-body {
background-color: $bg-dark-mode !important;
}
.text-light {
color: $text-color-dark-mode !important;
}
.border {
border-color: $border-color-dark-mode !important;
border-color: var(--bs-border-color) !important;
}
.border-end {
border-right: 1px solid $border-color-dark-mode !important;
border-right: 1px solid var(--bs-border-color) !important;
}
.border-start {
border-left: 1px solid $border-color-dark-mode !important;
border-left: 1px solid var(--bs-border-color) !important;
}
.border-bottom {
border-bottom: 1px solid $border-color-dark-mode !important;
border-bottom: 1px solid var(--bs-border-color) !important;
}
.nav-link {
color: $text-color-dark-mode !important;
.text-dark, .text-light {
color: var(--bs-body-color) !important;
}
&.active {
background-color: $bg-dark-mode;
color: $text-color-dark-mode;
border-color: $border-color-dark-mode $border-color-dark-mode $bg-dark-mode;
.close {
background-color: inherit !important;
}
}
&:hover {
color: $text-color-dark-mode-accent !important;
border-color: $border-color-dark-mode $border-color-dark-mode $bg-dark-mode;
.btn-outline-primary, .btn-primary {
&:hover, &:focus, &.active, &:active {
color: var(--ngx-body-color-accent) !important;
}
}
.page-item.active .page-link {
background-color: darken($primary-dark-mode, 10%);
}
.nav-tabs {
border-color: $border-color-dark-mode;
.nav-link {
color: $primary-dark-mode !important;
&.active {
color: $text-color-dark-mode !important;
}
}
}
.dropdown-menu {
background-color: $bg-light-dark-mode;
.dropdown-divider {
border-color: $border-color-dark-mode;
}
.dropdown-item {
color: $text-color-dark-mode;
&:hover {
background-color: $bg-light-dark-mode;
color: $text-color-dark-mode;
}
}
.dropdown-item.disabled {
color: darken($text-color-dark-mode, 20%);
.btn-outline-secondary {
&:hover, &:focus, &.active, &:active {
background-color: var(--ngx-bg-darker);
color: var(--bs-primary);
}
}
.card {
background-color: $bg-light-dark-mode;
background-color: var(--bs-body-bg);
.card-text {
color: $text-color-dark-mode;
.card-header {
background-color: rgba(0, 0, 0, 0.12);
}
}
.text-dark {
color: $text-color-dark-mode !important;
}
.modal-content, .modal-header, .modal-body, .modal-footer {
background-color: $bg-light-dark-mode;
border-color: $border-color-dark-mode;
background-color: var(--bs-body-bg);
border-color: var(--bs-border-color);
}
app-tag .badge {
filter: brightness(.8);
}
.badge-light {
background-color: darken($bg-dark-mode, 20%);
color: $text-color-dark-mode-accent;
}
.badge.border-light {
border-color: $bg-dark-mode !important;
}
.doc-img-container {
border: none !important;
border-top-left-radius: .25rem;
@ -157,8 +101,9 @@ $border-color-dark-mode: #47494f;
.doc-img {
mix-blend-mode: normal;
border-radius: 0;
border-color: $bg-dark-mode;
border-color: var(--bs-border-color);
filter: invert(10%);
&.border-end {
border-right: none !important;
}
@ -172,34 +117,14 @@ $border-color-dark-mode: #47494f;
mix-blend-mode: luminosity;
}
.toast {
background-color: opacify($bg-light-dark-mode, .85);
}
.toast-header {
background-color: opacify($bg-dark-mode, .85);
}
a,
.card-title a {
color: $primary-dark-mode;
&:hover {
color: lighten($primary, 10%);
}
.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 {
background-color: $bg-dark-mode;
color: $text-color-dark-mode;
border-color: $border-color-dark-mode;
.des,
.asc {
background-color: transparent !important;
color: $text-color-dark-mode;
border-color: $border-color-dark-mode;
&::after {
filter: invert(0.8); /* arrow is a black inline png bkgd image (!) so use filter */
}
@ -209,238 +134,31 @@ $border-color-dark-mode: #47494f;
background-color: $bg-light-dark-mode;
color: $text-color-dark-mode-accent;
}
}
.table td,
.table th {
border-color: $border-color-dark-mode;
}
.table-row-selected {
background-color: $bg-light-dark-mode;
}
.table-striped > tbody > tr:nth-of-type(odd) > * {
color: $text-color-dark-mode-accent;
}
.close {
color: $text-color-dark-mode;
.close, .modal .btn-close {
text-shadow: 0 1px 0 #666;
}
.modal .btn-close {
color: $text-color-dark-mode;
text-shadow: 0 1px 0 #666;
filter: invert(1) grayscale(100%) brightness(200%);
}
.btn-outline-primary {
border-color: $primary-dark-mode;
color: $primary-dark-mode;
&:not(:disabled):not(.disabled).active,
&:not(:disabled):not(.disabled):hover {
background-color: darken($primary-dark-mode, 10%);
border-color: darken($primary-dark-mode, 10%);
color: $text-color-dark-mode-accent;
}
&.disabled.active {
background-color: darken($primary-dark-mode, 10%);
}
}
.btn-outline-secondary {
border-color: darken($text-color-dark-mode, 30%);
color: $text-color-dark-mode;
&:not(:disabled):not(.disabled):hover {
background-color: $bg-dark-mode;
}
}
.btn-outline-danger {
border-color: $danger-dark-mode;
color: $danger-dark-mode;
&:not(:disabled):not(.disabled):hover {
background-color: darken($danger-dark-mode, 10%);
border-color: darken($danger-dark-mode, 10%);
color: $text-color-dark-mode-accent;
}
}
.btn-outline-dark {
border-color: $border-color-dark-mode;
color: $text-color-dark-mode;
&:not(:disabled):not(.disabled):hover {
color: $text-color-dark-mode-accent;
}
}
.btn-light:not(:disabled):not(.disabled) {
background-color: $bg-dark-mode;
color: $text-color-dark-mode-accent;
&:hover {
background-color: $text-color-dark-mode;
color: $bg-dark-mode;
}
}
.btn-link:not(:disabled):not(.disabled) {
color: $primary-dark-mode;
}
.btn-link:hover,
.btn-outline-primary:not(:disabled):not(.disabled).active,
.btn-outline-primary:not(:disabled):not(.disabled):active,
.show > .btn-outline-primary.dropdown-toggle {
color: $text-color-dark-mode-accent;
}
button.bg-light:hover {
background-color: $bg-dark-mode !important;
}
.card-footer button:hover {
color: $primary-dark-mode !important;
}
.form-control:not(.is-invalid):not(.btn),
input:not(.is-invalid),
textarea:not(.is-invalid) {
border-color: $border-color-dark-mode; /* we dont want to override controls that get highlighting for errors */
}
.form-control:not(.btn),
input,
select,
textarea {
background-color: $bg-dark-mode;
color: $text-color-dark-mode;
&::placeholder {
color: $text-color-dark-mode;
}
&:focus {
background-color: $bg-light-dark-mode !important;
color: darken($text-color-dark-mode, 10%) !important;
}
}
.form-select:not(.is-invalid):not(:disabled) {
border-color: $border-color-dark-mode;
}
.ng-select-container,
.ng-select.ng-select-opened > .ng-select-container,
.ng-dropdown-panel,
.ng-dropdown-panel .ng-dropdown-panel-items .ng-option {
background-color: $bg-dark-mode;
color: $text-color-dark-mode;
border-color: $border-color-dark-mode;
input:focus {
background-color: transparent !important;
}
}
.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;
}
.input-group-text {
color: $text-color-dark-mode;
background-color: $bg-light-dark-mode;
border-color: $border-color-dark-mode;
}
.list-group-item {
color: $text-color-dark-mode;
background-color: $bg-light-dark-mode;
border-color: $border-color-dark-mode;
}
.page-item.disabled .page-link {
background-color: $bg-dark-mode;
border-color: $border-color-dark-mode;
}
.list-group-item,
.page-link {
background-color: $bg-light-dark-mode;
border-color: $border-color-dark-mode;
}
.page-item.active .page-link {
border-color: $border-color-dark-mode;
color: $text-color-dark-mode-accent;
}
.progress {
background-color: $border-color-dark-mode;
}
.alert-danger {
color: $text-color-dark-mode-accent;
background-color: darken($danger-dark-mode, 20%);
border-color: darken($danger-dark-mode, 20%);
}
.bg-dark {
background-color: $bg-light-dark-mode !important;
}
.ngx-file-drop__drop-zone--over {
background-color: darken($primary-dark-mode, 35%) !important;
background-color: var(--ngx-primary-darken-15) !important;
}
.alert-secondary {
background-color: $bg-light-dark-mode;
border-color: darken($bg-light-dark-mode, 10%);
color: $text-color-dark-mode-accent;
.toast {
background-color: hsla(var(--pngx-primary), calc(var(--pngx-primary-lightness) - 18%), 0.9);
}
.progress-bar.bg-primary {
background-color: darken($primary-dark-mode, 5%) !important;
}
.popover {
.popover-header,
.popover-body {
background-color: $bg-dark-mode-accent;
border-color: $border-color-dark-mode;
}
}
// fix popover carat colors
.bs-popover-start > .popover-arrow::after, .bs-popover-auto[data-popper-placement^="left"] {
border-left-color: $bg-dark-mode-accent;
}
.bs-popover-end > .popover-arrow::after, .bs-popover-auto[data-popper-placement^="right"] {
border-right-color: $bg-dark-mode-accent;
}
.bs-popover-top > .popover-arrow::after, .bs-popover-auto[data-popper-placement^="top"] {
border-top-color: $bg-dark-mode-accent;
}
.bs-popover-bottom > .popover-arrow::after, .bs-popover-auto[data-popper-placement^="bottom"] {
border-bottom-color: $bg-dark-mode-accent;
}
.bs-popover-bottom .popover-header::before,
.bs-popover-auto[x-placement^=bottom] .popover-header::before {
border-bottom-color: $bg-dark-mode-accent;
}
.ngb-dp-header,
.ngb-dp-weekdays,
.ngb-dp-month {
background-color: $bg-light-dark-mode;
.toast-header {
background-color: hsla(var(--pngx-primary), calc(var(--pngx-primary-lightness) - 10%), 0.9);
}
}