feat: add theme picker (#69)
Add theme picker for configuration of angular material default themes. Fixes #68
This commit is contained in:
+43
-2
@@ -22,6 +22,7 @@
|
|||||||
"main": "src/main.ts",
|
"main": "src/main.ts",
|
||||||
"tsConfig": "src/tsconfig.app.json",
|
"tsConfig": "src/tsconfig.app.json",
|
||||||
"polyfills": "src/polyfills.ts",
|
"polyfills": "src/polyfills.ts",
|
||||||
|
"extractCss": true,
|
||||||
"assets": [
|
"assets": [
|
||||||
"src/favicon.ico",
|
"src/favicon.ico",
|
||||||
"src/robots.txt",
|
"src/robots.txt",
|
||||||
@@ -29,7 +30,27 @@
|
|||||||
"src/assets"
|
"src/assets"
|
||||||
],
|
],
|
||||||
"styles": [
|
"styles": [
|
||||||
"src/main.scss"
|
"src/main.scss",
|
||||||
|
{
|
||||||
|
"input": "src/theme/deeppurple-amber.scss",
|
||||||
|
"bundleName": "theme/deeppurple-amber",
|
||||||
|
"lazy": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "src/theme/indigo-pink.scss",
|
||||||
|
"bundleName": "theme/indigo-pink",
|
||||||
|
"lazy": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "src/theme/pink-bluegrey.scss",
|
||||||
|
"bundleName": "theme/pink-bluegrey",
|
||||||
|
"lazy": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "src/theme/purple-green.scss",
|
||||||
|
"bundleName": "theme/purple-green",
|
||||||
|
"lazy": true
|
||||||
|
}
|
||||||
],
|
],
|
||||||
"scripts": []
|
"scripts": []
|
||||||
},
|
},
|
||||||
@@ -80,7 +101,27 @@
|
|||||||
"tsConfig": "src/tsconfig.spec.json",
|
"tsConfig": "src/tsconfig.spec.json",
|
||||||
"scripts": [],
|
"scripts": [],
|
||||||
"styles": [
|
"styles": [
|
||||||
"src/main.scss"
|
"src/main.scss",
|
||||||
|
{
|
||||||
|
"input": "src/theme/deeppurple-amber.scss",
|
||||||
|
"bundleName": "theme/deeppurple-amber",
|
||||||
|
"lazy": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "src/theme/indigo-pink.scss",
|
||||||
|
"bundleName": "theme/indigo-pink",
|
||||||
|
"lazy": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "src/theme/pink-bluegrey.scss",
|
||||||
|
"bundleName": "theme/pink-bluegrey",
|
||||||
|
"lazy": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "src/theme/purple-green.scss",
|
||||||
|
"bundleName": "theme/purple-green",
|
||||||
|
"lazy": true
|
||||||
|
}
|
||||||
],
|
],
|
||||||
"assets": [
|
"assets": [
|
||||||
"src/favicon.ico",
|
"src/favicon.ico",
|
||||||
|
|||||||
Generated
+5
@@ -685,6 +685,11 @@
|
|||||||
"webpack-sources": "1.1.0"
|
"webpack-sources": "1.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@ngx-pwa/local-storage": {
|
||||||
|
"version": "6.0.0-rc.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ngx-pwa/local-storage/-/local-storage-6.0.0-rc.0.tgz",
|
||||||
|
"integrity": "sha512-ks+5nSjSbErv8u22jsu4zKUDcQuXTum9k5qVptq2+mytzxXfNZLMNz3NT91Ww72axgsoDKBNEYNaLuY4ST5pLg=="
|
||||||
|
},
|
||||||
"@ngx-rocket/ascii-logo": {
|
"@ngx-rocket/ascii-logo": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@ngx-rocket/ascii-logo/-/ascii-logo-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@ngx-rocket/ascii-logo/-/ascii-logo-1.1.0.tgz",
|
||||||
|
|||||||
@@ -37,6 +37,7 @@
|
|||||||
"@angular/platform-browser-dynamic": "^6.0.6",
|
"@angular/platform-browser-dynamic": "^6.0.6",
|
||||||
"@angular/router": "^6.0.6",
|
"@angular/router": "^6.0.6",
|
||||||
"@angular/service-worker": "^6.0.6",
|
"@angular/service-worker": "^6.0.6",
|
||||||
|
"@ngx-pwa/local-storage": "^6.0.0-rc.0",
|
||||||
"@ngx-translate/core": "^10.0.2",
|
"@ngx-translate/core": "^10.0.2",
|
||||||
"@swimlane/ngx-charts": "^8.1.0",
|
"@swimlane/ngx-charts": "^8.1.0",
|
||||||
"angular-archwizard": "^3.0.0",
|
"angular-archwizard": "^3.0.0",
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ import { CacheInterceptor } from './http/cache.interceptor';
|
|||||||
|
|
||||||
import { ShellComponent } from './shell/shell.component';
|
import { ShellComponent } from './shell/shell.component';
|
||||||
import { SidenavComponent } from './shell/sidenav/sidenav.component';
|
import { SidenavComponent } from './shell/sidenav/sidenav.component';
|
||||||
|
import { ThemePickerComponent } from './shell/toolbar/theme-picker/theme-picker.component';
|
||||||
import { ToolbarComponent } from './shell/toolbar/toolbar.component';
|
import { ToolbarComponent } from './shell/toolbar/toolbar.component';
|
||||||
import { ContentComponent } from './shell/content/content.component';
|
import { ContentComponent } from './shell/content/content.component';
|
||||||
import { BreadcrumbComponent } from './shell/breadcrumb/breadcrumb.component';
|
import { BreadcrumbComponent } from './shell/breadcrumb/breadcrumb.component';
|
||||||
@@ -102,6 +103,7 @@ import { BreadcrumbComponent } from './shell/breadcrumb/breadcrumb.component';
|
|||||||
declarations: [
|
declarations: [
|
||||||
ShellComponent,
|
ShellComponent,
|
||||||
SidenavComponent,
|
SidenavComponent,
|
||||||
|
ThemePickerComponent,
|
||||||
ToolbarComponent,
|
ToolbarComponent,
|
||||||
ContentComponent,
|
ContentComponent,
|
||||||
BreadcrumbComponent
|
BreadcrumbComponent
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { TestBed, inject } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ThemeManagerService } from './theme-manager.service';
|
||||||
|
|
||||||
|
describe('ThemeManagerService', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
providers: [ThemeManagerService]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', inject([ThemeManagerService], (service: ThemeManagerService) => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
}));
|
||||||
|
});
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class ThemeManagerService {
|
||||||
|
|
||||||
|
private themeManagerClass = 'theme-manager';
|
||||||
|
|
||||||
|
constructor() { }
|
||||||
|
|
||||||
|
setTheme(href: string) {
|
||||||
|
this.getLinkElement().setAttribute('href', href);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeTheme() {
|
||||||
|
const existingLinkElement = this.getExistingLinkElement();
|
||||||
|
if (existingLinkElement) {
|
||||||
|
document.head.removeChild(existingLinkElement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getLinkElement() {
|
||||||
|
return this.getExistingLinkElement() || this.createLinkElement();
|
||||||
|
}
|
||||||
|
|
||||||
|
private getExistingLinkElement() {
|
||||||
|
return document.head.querySelector(`link[rel="stylesheet"].${this.themeManagerClass}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
private createLinkElement() {
|
||||||
|
const linkElement = document.createElement('link');
|
||||||
|
linkElement.setAttribute('rel', 'stylesheet');
|
||||||
|
linkElement.classList.add(this.themeManagerClass);
|
||||||
|
document.head.appendChild(linkElement);
|
||||||
|
return linkElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<button mat-icon-button [matMenuTriggerFor]="themeMenu" matTooltip="Color Schemes">
|
||||||
|
<mat-icon>format_color_fill</mat-icon>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<mat-menu class="mifosx-theme-picker-menu" #themeMenu="matMenu" x-position="before">
|
||||||
|
<mat-grid-list cols="2">
|
||||||
|
<mat-grid-tile *ngFor="let theme of themes">
|
||||||
|
<div mat-menu-item (click)="installTheme(theme)">
|
||||||
|
<div class="mifosx-theme-picker-swatch">
|
||||||
|
<mat-icon class="mifosx-theme-chosen-icon" *ngIf="currentTheme.href === theme.href">check_circle</mat-icon>
|
||||||
|
<div class="mifosx-theme-picker-primary" [style.background]="theme.primary"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</mat-grid-tile>
|
||||||
|
</mat-grid-list>
|
||||||
|
</mat-menu>
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
$theme-picker-menu-padding: 8px;
|
||||||
|
$theme-picker-grid-cell-size: 48px;
|
||||||
|
$theme-picker-grid-cells-per-row: 2;
|
||||||
|
$theme-picker-swatch-size: 36px;
|
||||||
|
$theme-picker-accent-stripe-size: 6px;
|
||||||
|
|
||||||
|
.mifosx-theme-picker-menu {
|
||||||
|
.mat-menu-content {
|
||||||
|
padding: $theme-picker-menu-padding;
|
||||||
|
}
|
||||||
|
|
||||||
|
[mat-menu-item] {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mifosx-theme-picker-swatch {
|
||||||
|
position: relative;
|
||||||
|
width: $theme-picker-swatch-size;
|
||||||
|
height: $theme-picker-swatch-size;
|
||||||
|
margin: ($theme-picker-grid-cell-size - $theme-picker-swatch-size) / 2;
|
||||||
|
border-radius: 50%;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.mifosx-theme-chosen-icon {
|
||||||
|
color: white;
|
||||||
|
position: absolute;
|
||||||
|
left: 50%; top: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: 1px solid rgba(0,0,0,.2);
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mifosx-theme-picker-primary {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mifosx-theme-picker-accent {
|
||||||
|
position: absolute;
|
||||||
|
bottom: $theme-picker-accent-stripe-size;
|
||||||
|
width: 100%;
|
||||||
|
height: $theme-picker-accent-stripe-size;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ThemePickerComponent } from './theme-picker.component';
|
||||||
|
|
||||||
|
describe('ThemePickerComponent', () => {
|
||||||
|
let component: ThemePickerComponent;
|
||||||
|
let fixture: ComponentFixture<ThemePickerComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [ ThemePickerComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(ThemePickerComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import { Component, OnInit, ViewEncapsulation } from '@angular/core';
|
||||||
|
|
||||||
|
import { Theme } from './theme.model';
|
||||||
|
import { ThemeManagerService } from './theme-manager.service';
|
||||||
|
import { ThemeStorageService } from './theme-storage.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'mifosx-theme-picker',
|
||||||
|
templateUrl: './theme-picker.component.html',
|
||||||
|
styleUrls: ['./theme-picker.component.scss'],
|
||||||
|
encapsulation: ViewEncapsulation.None
|
||||||
|
})
|
||||||
|
export class ThemePickerComponent implements OnInit {
|
||||||
|
|
||||||
|
currentTheme: Theme = {
|
||||||
|
href: 'indigo-pink.css',
|
||||||
|
primary: '#3F51B5',
|
||||||
|
accent: '#E91E63',
|
||||||
|
isDark: false,
|
||||||
|
isDefault: true,
|
||||||
|
};
|
||||||
|
themes = [
|
||||||
|
this.currentTheme,
|
||||||
|
{
|
||||||
|
href: 'deeppurple-amber.css',
|
||||||
|
primary: '#673AB7',
|
||||||
|
accent: '#FFC107',
|
||||||
|
isDark: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: 'pink-bluegrey.css',
|
||||||
|
primary: '#E91E63',
|
||||||
|
accent: '#607D8B',
|
||||||
|
isDark: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: 'purple-green.css',
|
||||||
|
primary: '#9C27B0',
|
||||||
|
accent: '#4CAF50',
|
||||||
|
isDark: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
constructor(public themeManagerService: ThemeManagerService,
|
||||||
|
private themeStorageService: ThemeStorageService) {
|
||||||
|
this.themeStorageService.getTheme().subscribe((theme) => {
|
||||||
|
this.installTheme(theme);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit() { }
|
||||||
|
|
||||||
|
installTheme(theme: Theme) {
|
||||||
|
if (theme) {
|
||||||
|
this.currentTheme = theme;
|
||||||
|
}
|
||||||
|
if (this.currentTheme.isDefault) {
|
||||||
|
this.themeManagerService.removeTheme();
|
||||||
|
} else {
|
||||||
|
this.themeManagerService.setTheme(`theme/${this.currentTheme.href}`);
|
||||||
|
}
|
||||||
|
this.themeStorageService.storeTheme(this.currentTheme);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { TestBed, inject } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ThemeStorageService } from './theme-storage.service';
|
||||||
|
|
||||||
|
describe('ThemeStorageService', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
providers: [ThemeStorageService]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', inject([ThemeStorageService], (service: ThemeStorageService) => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
}));
|
||||||
|
});
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import {Injectable, EventEmitter} from '@angular/core';
|
||||||
|
|
||||||
|
import { LocalStorage } from '@ngx-pwa/local-storage';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
|
import { Theme } from './theme.model';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class ThemeStorageService {
|
||||||
|
|
||||||
|
private themeStorageKey = 'mifosXTheme';
|
||||||
|
onThemeUpdate: EventEmitter<Theme>;
|
||||||
|
|
||||||
|
constructor(private localStorage: LocalStorage) {
|
||||||
|
this.onThemeUpdate = new EventEmitter<Theme>();
|
||||||
|
}
|
||||||
|
|
||||||
|
storeTheme(mifosXTheme: Theme) {
|
||||||
|
this.localStorage.setItem(this.themeStorageKey, mifosXTheme)
|
||||||
|
.subscribe(() => this.onThemeUpdate.emit(mifosXTheme));
|
||||||
|
}
|
||||||
|
|
||||||
|
getTheme(): Observable<Theme> {
|
||||||
|
return this.localStorage.getItem<Theme>(this.themeStorageKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
clearTheme() {
|
||||||
|
this.localStorage.removeItem(this.themeStorageKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
export interface Theme {
|
||||||
|
href: string;
|
||||||
|
primary: string;
|
||||||
|
accent: string;
|
||||||
|
isDark?: boolean;
|
||||||
|
isDefault?: boolean;
|
||||||
|
}
|
||||||
@@ -56,9 +56,7 @@
|
|||||||
<mat-option value="Hindi">Hindi</mat-option>
|
<mat-option value="Hindi">Hindi</mat-option>
|
||||||
</mat-select>
|
</mat-select>
|
||||||
|
|
||||||
<button mat-icon-button class="ml-1" matTooltip="Color Schemes" fxHide.lt-md>
|
<mifosx-theme-picker class="ml-1" fxHide.lt-md></mifosx-theme-picker>
|
||||||
<mat-icon>format_color_fill</mat-icon>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button mat-icon-button class="ml-1" matTooltip="Notifications">
|
<button mat-icon-button class="ml-1" matTooltip="Notifications">
|
||||||
<mat-icon>notifications</mat-icon>
|
<mat-icon>notifications</mat-icon>
|
||||||
|
|||||||
+1
-1
@@ -3,7 +3,7 @@
|
|||||||
* Component-specific style should not go here and be included directly as part of the components.
|
* Component-specific style should not go here and be included directly as part of the components.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Theme customization
|
// Import Default Theme for MifosX
|
||||||
@import "~@angular/material/prebuilt-themes/indigo-pink.css";
|
@import "~@angular/material/prebuilt-themes/indigo-pink.css";
|
||||||
|
|
||||||
// 3rd party libraries
|
// 3rd party libraries
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
@import "~@angular/material/prebuilt-themes/deeppurple-amber.css";
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@import "~@angular/material/prebuilt-themes/indigo-pink.css";
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@import "~@angular/material/prebuilt-themes/pink-bluegrey.css";
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@import "~@angular/material/prebuilt-themes/purple-green.css";
|
||||||
Reference in New Issue
Block a user