Implement ripple effect for material themes

This commit is contained in:
eugene-sinitsyn 2020-03-04 19:31:08 +03:00 committed by Maksim Karatkevich
parent c7bcbca7bd
commit 2f7d61ded1
12 changed files with 334 additions and 15 deletions

20
package-lock.json generated
View file

@ -363,18 +363,17 @@
"integrity": "sha512-zTCgrIAA9FYPMbqqpQnoNltiLR58q0FMfzP2t96q/1tjyVy/Y/IaNgVQ7eL0HeQ0nG6IAzQ1HVx8Xeneg4Yj5Q==" "integrity": "sha512-zTCgrIAA9FYPMbqqpQnoNltiLR58q0FMfzP2t96q/1tjyVy/Y/IaNgVQ7eL0HeQ0nG6IAzQ1HVx8Xeneg4Yj5Q=="
}, },
"@angular/cdk": { "@angular/cdk": {
"version": "8.0.0", "version": "9.1.0",
"resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-8.0.0.tgz", "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-9.1.0.tgz",
"integrity": "sha512-2vsRWEHNARe0iRmqgzvM67gwfRy+aKvdef4Qu9L+ndSsTrrZT3tSgG8SMn1v9SfBHnx5G8mo4d1AMquXG69AuQ==", "integrity": "sha512-qKpAudJx9z0MD+ADptS0OZViJBTA49+JCKym0hPQUkcB9Po4Al6gu6oZ1VSXV5Ln3T84z9aAw/AhUGP/YCFNSQ==",
"requires": { "requires": {
"parse5": "^5.0.0", "parse5": "^5.0.0"
"tslib": "^1.7.1"
}, },
"dependencies": { "dependencies": {
"parse5": { "parse5": {
"version": "5.1.0", "version": "5.1.1",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.0.tgz", "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz",
"integrity": "sha512-fxNG2sQjHvlVAYmzBZS9YlDp6PTSSDwa98vkD4QgVDDCAo84z5X1t5XyJQ62ImdLXx5NdIIfihey6xpum9/gRQ==", "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==",
"optional": true "optional": true
} }
} }
@ -857,6 +856,11 @@
"integrity": "sha512-Q/kFQV4mjZ/Mpx6GksriM5lingjX73EwtVc79AfVMA76Pv5XqfYQZuti6tk7DvYQD89sv1Z/iN2di+ZLKsSTnw==", "integrity": "sha512-Q/kFQV4mjZ/Mpx6GksriM5lingjX73EwtVc79AfVMA76Pv5XqfYQZuti6tk7DvYQD89sv1Z/iN2di+ZLKsSTnw==",
"dev": true "dev": true
}, },
"@angular/material": {
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/@angular/material/-/material-9.1.0.tgz",
"integrity": "sha512-KjA1ARoZ1vBg4jUH1y39muynxAFvkD8QZFGKH8Nh4zXW5pOP/7NyjokxXsNx5qSBCq/6ac2xBxCrCYJN7HmoDw=="
},
"@angular/platform-browser": { "@angular/platform-browser": {
"version": "9.0.4", "version": "9.0.4",
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-9.0.4.tgz", "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-9.0.4.tgz",

View file

@ -31,12 +31,13 @@
}, },
"dependencies": { "dependencies": {
"@angular/animations": "^9.0.4", "@angular/animations": "^9.0.4",
"@angular/cdk": "^8.0.0", "@angular/cdk": "^9.1.0",
"@angular/common": "^9.0.4", "@angular/common": "^9.0.4",
"@angular/compiler": "^9.0.4", "@angular/compiler": "^9.0.4",
"@angular/core": "^9.0.4", "@angular/core": "^9.0.4",
"@angular/forms": "^9.0.4", "@angular/forms": "^9.0.4",
"@angular/google-maps": "^9.1.0", "@angular/google-maps": "^9.1.0",
"@angular/material": "^9.1.0",
"@angular/platform-browser": "^9.0.4", "@angular/platform-browser": "^9.0.4",
"@angular/platform-browser-dynamic": "^9.0.4", "@angular/platform-browser-dynamic": "^9.0.4",
"@angular/router": "^9.0.4", "@angular/router": "^9.0.4",

View file

@ -1,5 +1,6 @@
import { ModuleWithProviders, NgModule, Optional, SkipSelf } from '@angular/core'; import { ModuleWithProviders, NgModule, Optional, SkipSelf } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { MAT_RIPPLE_GLOBAL_OPTIONS } from '@angular/material/core';
import { NbAuthModule, NbDummyAuthStrategy } from '@nebular/auth'; import { NbAuthModule, NbDummyAuthStrategy } from '@nebular/auth';
import { NbSecurityModule, NbRoleProvider } from '@nebular/security'; import { NbSecurityModule, NbRoleProvider } from '@nebular/security';
import { of as observableOf } from 'rxjs'; import { of as observableOf } from 'rxjs';
@ -51,6 +52,7 @@ import { CountryOrderService } from './mock/country-order.service';
import { StatsProgressBarService } from './mock/stats-progress-bar.service'; import { StatsProgressBarService } from './mock/stats-progress-bar.service';
import { VisitorsAnalyticsService } from './mock/visitors-analytics.service'; import { VisitorsAnalyticsService } from './mock/visitors-analytics.service';
import { SecurityCamerasService } from './mock/security-cameras.service'; import { SecurityCamerasService } from './mock/security-cameras.service';
import { RippleService } from './utils/ripple.service';
import { MockDataModule } from './mock/mock-data.module'; import { MockDataModule } from './mock/mock-data.module';
const socialLinks = [ const socialLinks = [
@ -91,6 +93,7 @@ const DATA_SERVICES = [
{ provide: StatsProgressBarData, useClass: StatsProgressBarService }, { provide: StatsProgressBarData, useClass: StatsProgressBarService },
{ provide: VisitorsAnalyticsData, useClass: VisitorsAnalyticsService }, { provide: VisitorsAnalyticsData, useClass: VisitorsAnalyticsService },
{ provide: SecurityCamerasData, useClass: SecurityCamerasService }, { provide: SecurityCamerasData, useClass: SecurityCamerasService },
{provide: MAT_RIPPLE_GLOBAL_OPTIONS, useExisting: RippleService},
]; ];
export class NbSimpleRoleProvider extends NbRoleProvider { export class NbSimpleRoleProvider extends NbRoleProvider {

View file

@ -0,0 +1,11 @@
import { Injectable } from '@angular/core';
import { RippleGlobalOptions } from '@angular/material/core';
@Injectable({providedIn: 'root'})
export class RippleService implements RippleGlobalOptions {
public disabled: boolean = false;
public toggle(enabled: boolean): void {
this.disabled = !enabled;
}
}

View file

@ -2,8 +2,10 @@ import { Component, OnDestroy, OnInit } from '@angular/core';
import { NbMediaBreakpointsService, NbMenuService, NbSidebarService, NbThemeService } from '@nebular/theme'; import { NbMediaBreakpointsService, NbMenuService, NbSidebarService, NbThemeService } from '@nebular/theme';
import { UserData } from '../../../@core/data/users'; import { UserData } from '../../../@core/data/users';
import { LayoutService } from '../../../@core/utils';
import { map, takeUntil } from 'rxjs/operators'; import { map, takeUntil } from 'rxjs/operators';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { RippleService } from '../../../@core/utils/ripple.service';
@Component({ @Component({
selector: 'ngx-header', selector: 'ngx-header',
@ -51,7 +53,9 @@ export class HeaderComponent implements OnInit, OnDestroy {
private menuService: NbMenuService, private menuService: NbMenuService,
private themeService: NbThemeService, private themeService: NbThemeService,
private userService: UserData, private userService: UserData,
private breakpointService: NbMediaBreakpointsService) { private layoutService: LayoutService,
private breakpointService: NbMediaBreakpointsService,
private rippleService: RippleService) {
} }
ngOnInit() { ngOnInit() {
@ -74,7 +78,10 @@ export class HeaderComponent implements OnInit, OnDestroy {
map(({ name }) => name), map(({ name }) => name),
takeUntil(this.destroy$), takeUntil(this.destroy$),
) )
.subscribe(themeName => this.currentTheme = themeName); .subscribe(themeName => {
this.currentTheme = themeName;
this.rippleService.toggle(themeName?.startsWith('material'));
});
} }
ngOnDestroy() { ngOnDestroy() {
@ -88,6 +95,7 @@ export class HeaderComponent implements OnInit, OnDestroy {
toggleSidebar(): boolean { toggleSidebar(): boolean {
this.sidebarService.toggle(true, 'menu-sidebar'); this.sidebarService.toggle(true, 'menu-sidebar');
this.layoutService.changeLayoutSize();
return false; return false;
} }

View file

@ -0,0 +1,21 @@
@mixin ngx-ripple() {
.mat-ripple {
overflow: hidden;
position: relative;
&:not(:empty) {
transform: translateZ(0);
}
}
.mat-ripple-element {
position: absolute;
border-radius: 50%;
pointer-events: none;
transition: opacity, transform 0ms cubic-bezier(0, 0, 0.2, 1);
transform: scale(0);
// switch ripple color depending on selected theme
background-color: nb-theme(background-alternative-color-4);
opacity: .1;
}
}

View file

@ -18,6 +18,7 @@
@import './layout'; @import './layout';
@import './overrides'; @import './overrides';
@import './ripple';
// install the framework and custom global styles // install the framework and custom global styles
@include nb-install() { @include nb-install() {
@ -31,4 +32,5 @@
@include ngx-pace-theme(); @include ngx-pace-theme();
@include nb-overrides(); @include nb-overrides();
@include ngx-ripple();
}; };

View file

@ -1,5 +1,6 @@
import { ModuleWithProviders, NgModule } from '@angular/core'; import { ModuleWithProviders, NgModule } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { MatRippleModule } from '@angular/material/core';
import { import {
NbActionsModule, NbActionsModule,
NbLayoutModule, NbLayoutModule,
@ -71,8 +72,8 @@ const PIPES = [
]; ];
@NgModule({ @NgModule({
imports: [CommonModule, ...NB_MODULES], imports: [CommonModule, MatRippleModule, ...NB_MODULES],
exports: [CommonModule, ...PIPES, ...COMPONENTS], exports: [CommonModule, MatRippleModule, ...PIPES, ...COMPONENTS],
declarations: [...COMPONENTS, ...PIPES], declarations: [...COMPONENTS, ...PIPES],
}) })
export class ThemeModule { export class ThemeModule {

View file

@ -1,8 +1,103 @@
import { Component } from '@angular/core'; import {Component, OnDestroy} from '@angular/core';
import { NbThemeService } from '@nebular/theme';
import { takeWhile } from 'rxjs/operators' ;
import { SolarData } from '../../@core/data/solar';
interface CardSettings {
title: string;
iconClass: string;
type: string;
}
@Component({ @Component({
selector: 'ngx-dashboard', selector: 'ngx-dashboard',
styleUrls: ['./dashboard.component.scss'],
templateUrl: './dashboard.component.html', templateUrl: './dashboard.component.html',
}) })
export class DashboardComponent { export class DashboardComponent implements OnDestroy {
private alive = true;
solarValue: number;
lightCard: CardSettings = {
title: 'Light',
iconClass: 'nb-lightbulb',
type: 'primary',
};
rollerShadesCard: CardSettings = {
title: 'Roller Shades',
iconClass: 'nb-roller-shades',
type: 'success',
};
wirelessAudioCard: CardSettings = {
title: 'Wireless Audio',
iconClass: 'nb-audio',
type: 'info',
};
coffeeMakerCard: CardSettings = {
title: 'Coffee Maker',
iconClass: 'nb-coffee-maker',
type: 'warning',
};
statusCards: string;
commonStatusCardsSet: CardSettings[] = [
this.lightCard,
this.rollerShadesCard,
this.wirelessAudioCard,
this.coffeeMakerCard,
];
statusCardsByThemes: {
default: CardSettings[];
cosmic: CardSettings[];
corporate: CardSettings[];
dark: CardSettings[];
'material-dark': CardSettings[];
'material-light': CardSettings[];
} = {
default: this.commonStatusCardsSet,
cosmic: this.commonStatusCardsSet,
corporate: [
{
...this.lightCard,
type: 'warning',
},
{
...this.rollerShadesCard,
type: 'primary',
},
{
...this.wirelessAudioCard,
type: 'danger',
},
{
...this.coffeeMakerCard,
type: 'info',
},
],
dark: this.commonStatusCardsSet,
'material-dark': this.commonStatusCardsSet,
'material-light': this.commonStatusCardsSet,
};
constructor(private themeService: NbThemeService,
private solarService: SolarData) {
this.themeService.getJsTheme()
.pipe(takeWhile(() => this.alive))
.subscribe(theme => {
this.statusCards = this.statusCardsByThemes[theme.name];
});
this.solarService.getSolarData()
.pipe(takeWhile(() => this.alive))
.subscribe((data) => {
this.solarValue = data;
});
}
ngOnDestroy() {
this.alive = false;
}
} }

View file

@ -0,0 +1,78 @@
@import '../../../@theme/styles/themes';
@include nb-install-component() {
nb-card {
flex-direction: row;
align-items: center;
height: 6rem;
.icon-container {
height: 100%;
padding: 0.625rem;
}
.icon {
display: flex;
align-items: center;
justify-content: center;
width: 5.75rem;
height: 4.75rem;
font-size: 3.75rem;
border-radius: nb-theme(card-border-radius);
transition: width 0.4s ease;
transform: translate3d(0, 0, 0);
-webkit-transform-style: preserve-3d;
-webkit-backface-visibility: hidden;
color: nb-theme(text-control-color);
@each $status in nb-get-statuses() {
&.status-#{$status} {
$left-color: nb-theme(button-hero-#{$status}-left-background-color);
$right-color: nb-theme(button-hero-#{$status}-right-background-color);
background-image: linear-gradient(to right, $left-color, $right-color);
&:hover {
$left-hover-color: nb-theme(button-hero-#{$status}-hover-left-background-color);
$right-hover-color: nb-theme(button-hero-#{$status}-hover-right-background-color);
background-image: linear-gradient(to right, $left-hover-color, $right-hover-color);
}
}
}
}
&.off {
color: nb-theme(text-hint-color);
.status,
.title,
.icon {
color: nb-theme(text-hint-color);
}
@each $status in nb-get-statuses() {
.icon.status-#{$status} {
box-shadow: none;
background-image: linear-gradient(to right, transparent, transparent);
}
}
}
.details {
display: flex;
flex-direction: column;
justify-content: center;
height: 100%;
@include nb-ltr(padding, 0 0.5rem 0 0.75rem);
@include nb-rtl(padding, 0 0.75rem 0 0.5rem);
border-left: 1px solid transparent;
}
.title {
margin: 0;
}
.status {
text-transform: uppercase;
}
}
}

View file

@ -0,0 +1,26 @@
import { Component, Input } from '@angular/core';
@Component({
selector: 'ngx-status-card',
styleUrls: ['./status-card.component.scss'],
template: `
<nb-card matRipple (click)="on = !on" [ngClass]="{'off': !on}">
<div class="icon-container">
<div class="icon status-{{ type }}">
<ng-content></ng-content>
</div>
</div>
<div class="details">
<div class="title h5">{{ title }}</div>
<div class="status paragraph-2">{{ on ? 'ON' : 'OFF' }}</div>
</div>
</nb-card>
`,
})
export class StatusCardComponent {
@Input() title: string;
@Input() type: string;
@Input() on = true;
}

View file

@ -0,0 +1,69 @@
<nb-card size="large">
<nb-tabset fullWidth>
<nb-tab tabTitle="Temperature">
<div class="slider-container">
<ngx-temperature-dragger [(value)]="temperature" (power)="temperatureOff = !$event"
[min]="temperatureData.min" [max]="temperatureData.max" [disableArcColor]="theme.arcEmpty"
[fillColors]="theme.arcFill" [thumbBg]="theme.thumbBg" [thumbBorderColor]="theme.thumbBorder">
<div class="slider-value-container" [ngClass]="{ 'off': temperatureOff }">
<div class="value temperature h1">
{{ temperatureOff ? '--' : (temperature | ngxRound) }}
</div>
<div class="desc">
Celsius
</div>
</div>
</ngx-temperature-dragger>
</div>
<nb-radio-group [(ngModel)]="temperatureMode" name="temperature-mode">
<nb-radio matRipple value="cool">
<i class="nb-snowy-circled"></i>
</nb-radio>
<nb-radio matRipple value="warm">
<i class="nb-sunny-circled"></i>
</nb-radio>
<nb-radio matRipple value="heat">
<i class="nb-flame-circled"></i>
</nb-radio>
<nb-radio matRipple value="fan">
<i class="nb-loop-circled"></i>
</nb-radio>
</nb-radio-group>
</nb-tab>
<nb-tab tabTitle="Humidity">
<div class="slider-container">
<ngx-temperature-dragger [(value)]="humidity" (power)="humidityOff = !$event"
[min]="humidityData.min" [max]="humidityData.max" [disableArcColor]="theme.arcEmpty"
[fillColors]="theme.arcFill" [thumbBg]="theme.thumbBg" [thumbBorderColor]="theme.thumbBorder">
<div class="slider-value-container" [ngClass]="{ 'off': humidityOff }">
<div class="value humidity h1">
{{ humidityOff ? '--' : (humidity | ngxRound) }}
</div>
</div>
</ngx-temperature-dragger>
</div>
<nb-radio-group [(ngModel)]="humidityMode" name="humidity-mode">
<nb-radio value="cool">
<i class="nb-snowy-circled"></i>
</nb-radio>
<nb-radio value="warm">
<i class="nb-sunny-circled"></i>
</nb-radio>
<nb-radio value="heat">
<i class="nb-flame-circled"></i>
</nb-radio>
<nb-radio value="fan">
<i class="nb-loop-circled"></i>
</nb-radio>
</nb-radio-group>
</nb-tab>
</nb-tabset>
</nb-card>