mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 10:21:52 +00:00
Night theme (#2137)
* feat: add themes * fix: remove obvious issues with light theme * chore: improve light theme a bit * comment out theme swticher * chore: make login dark * add theme and widgets to seeds * add theme and widgets to migration --------- Co-authored-by: Matt Hill <matthewonthemoon@gmail.com>
This commit is contained in:
committed by
Aiden McClelland
parent
e867f31c31
commit
3c0a82293c
@@ -1,4 +1,4 @@
|
||||
<ion-item [routerLink]="['/marketplace', pkg.manifest.id]">
|
||||
<ion-item class="service-card" [routerLink]="['/marketplace', pkg.manifest.id]">
|
||||
<ion-thumbnail slot="start">
|
||||
<img alt="" [src]="'data:image/png;base64,' + pkg.icon | trustUrl" />
|
||||
</ion-thumbnail>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<ion-col responsiveCol class="column" sizeSm="8" sizeLg="6">
|
||||
<ion-toolbar color="transparent" class="ion-text-left">
|
||||
<ion-searchbar
|
||||
color="dark"
|
||||
[color]="(theme$ | async) === 'Light' ? 'light' : 'dark'"
|
||||
debounce="250"
|
||||
[ngModel]="query"
|
||||
(ngModelChange)="onModelChange($event)"
|
||||
|
||||
@@ -2,9 +2,11 @@ import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
EventEmitter,
|
||||
inject,
|
||||
Input,
|
||||
Output,
|
||||
} from '@angular/core'
|
||||
import { THEME } from '@start9labs/shared'
|
||||
|
||||
@Component({
|
||||
selector: 'marketplace-search',
|
||||
@@ -19,6 +21,8 @@ export class SearchComponent {
|
||||
@Output()
|
||||
readonly queryChange = new EventEmitter<string>()
|
||||
|
||||
readonly theme$ = inject(THEME)
|
||||
|
||||
onModelChange(query: string) {
|
||||
this.query = query
|
||||
this.queryChange.emit(query)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { NgModule } from '@angular/core'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
@@ -6,7 +7,7 @@ import { ResponsiveColModule } from '@start9labs/shared'
|
||||
import { SearchComponent } from './search.component'
|
||||
|
||||
@NgModule({
|
||||
imports: [IonicModule, FormsModule, ResponsiveColModule],
|
||||
imports: [IonicModule, FormsModule, CommonModule, ResponsiveColModule],
|
||||
declarations: [SearchComponent],
|
||||
exports: [SearchComponent],
|
||||
})
|
||||
|
||||
BIN
frontend/projects/shared/assets/img/logo_dark.png
Normal file
BIN
frontend/projects/shared/assets/img/logo_dark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
@@ -46,6 +46,11 @@ export * from './services/emver.service'
|
||||
export * from './services/error-toast.service'
|
||||
export * from './services/http.service'
|
||||
|
||||
export * from './themes/dark-theme/dark-theme.component'
|
||||
export * from './themes/dark-theme/dark-theme.module'
|
||||
export * from './themes/light-theme/light-theme.component'
|
||||
export * from './themes/light-theme/light-theme.module'
|
||||
|
||||
export * from './types/api'
|
||||
export * from './types/http.types'
|
||||
export * from './types/rpc.types'
|
||||
@@ -53,6 +58,7 @@ export * from './types/url'
|
||||
export * from './types/workspace-config'
|
||||
|
||||
export * from './tokens/relative-url'
|
||||
export * from './tokens/theme'
|
||||
|
||||
export * from './util/base-64'
|
||||
export * from './util/copy-to-clipboard'
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
@import '../../../styles/variables';
|
||||
@@ -0,0 +1,15 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
ViewEncapsulation,
|
||||
} from '@angular/core'
|
||||
import { AbstractTuiThemeSwitcher } from '@taiga-ui/cdk'
|
||||
|
||||
@Component({
|
||||
selector: 'dark-theme',
|
||||
template: '',
|
||||
styleUrls: ['./dark-theme.component.scss'],
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class DarkThemeComponent extends AbstractTuiThemeSwitcher {}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
|
||||
import { DarkThemeComponent } from './dark-theme.component'
|
||||
|
||||
@NgModule({
|
||||
declarations: [DarkThemeComponent],
|
||||
exports: [DarkThemeComponent],
|
||||
})
|
||||
export class DarkThemeModule {}
|
||||
@@ -0,0 +1,95 @@
|
||||
// Ionic Variables and Theming. For more info, please see:
|
||||
// http://ionicframework.com/docs/theming/
|
||||
|
||||
/** Ionic CSS Variables **/
|
||||
:root {
|
||||
--ion-color-primary: #0075e1;
|
||||
--ion-color-primary-rgb: 66, 140, 255;
|
||||
--ion-color-primary-contrast: #ffffff;
|
||||
--ion-color-primary-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-primary-shade: #3a7be0;
|
||||
--ion-color-primary-tint: #5598ff;
|
||||
|
||||
--ion-color-secondary: #50c8ff;
|
||||
--ion-color-secondary-rgb: 80, 200, 255;
|
||||
--ion-color-secondary-contrast: #ffffff;
|
||||
--ion-color-secondary-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-secondary-shade: #46b0e0;
|
||||
--ion-color-secondary-tint: #62ceff;
|
||||
|
||||
--ion-color-tertiary: #6a64ff;
|
||||
--ion-color-tertiary-rgb: 106, 100, 255;
|
||||
--ion-color-tertiary-contrast: #ffffff;
|
||||
--ion-color-tertiary-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-tertiary-shade: #5d58e0;
|
||||
--ion-color-tertiary-tint: #7974ff;
|
||||
|
||||
--ion-color-success: #2fdf75;
|
||||
--ion-color-success-rgb: 47, 223, 117;
|
||||
--ion-color-success-contrast: #000000;
|
||||
--ion-color-success-contrast-rgb: 0, 0, 0;
|
||||
--ion-color-success-shade: #29c467;
|
||||
--ion-color-success-tint: #44e283;
|
||||
|
||||
--ion-color-warning: #ffb506;
|
||||
--ion-color-warning-rgb: 255, 213, 52;
|
||||
--ion-color-warning-contrast: #000000;
|
||||
--ion-color-warning-contrast-rgb: 0, 0, 0;
|
||||
--ion-color-warning-shade: #e0bb2e;
|
||||
--ion-color-warning-tint: #ffd534;
|
||||
|
||||
--ion-color-danger: #ff4961;
|
||||
--ion-color-danger-rgb: 255, 73, 97;
|
||||
--ion-color-danger-contrast: #ffffff;
|
||||
--ion-color-danger-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-danger-shade: #e04055;
|
||||
--ion-color-danger-tint: #ff5b71;
|
||||
|
||||
//--ion-color-light: #f4f5f8;
|
||||
//--ion-color-light-rgb: 244, 245, 248;
|
||||
//--ion-color-light-contrast: #000000;
|
||||
//--ion-color-light-contrast-rgb: 0, 0, 0;
|
||||
//--ion-color-light-shade: #d7d8da;
|
||||
//--ion-color-light-tint: #f5f6f9;
|
||||
//
|
||||
//--ion-color-medium: #f4f5f8;
|
||||
//--ion-color-medium-rgb: 244, 245, 248;
|
||||
//--ion-color-medium-contrast: #000000;
|
||||
//--ion-color-medium-contrast-rgb: 0, 0, 0;
|
||||
//--ion-color-medium-shade: #d7d8da;
|
||||
//--ion-color-medium-tint: #f5f6f9;
|
||||
//
|
||||
//--ion-color-dark: #92949c;
|
||||
//--ion-color-dark-rgb: 146, 148, 156;
|
||||
//--ion-color-dark-contrast: #ffffff;
|
||||
//--ion-color-dark-contrast-rgb: 255, 255, 255;
|
||||
//--ion-color-dark-shade: #808289;
|
||||
//--ion-color-dark-tint: #9d9fa6;
|
||||
|
||||
--ion-color-step-50: #f2f2f2;
|
||||
--ion-color-step-100: #e6e6e6;
|
||||
--ion-color-step-150: #d9d9d9;
|
||||
--ion-color-step-200: #cccccc;
|
||||
--ion-color-step-250: #bfbfbf;
|
||||
--ion-color-step-300: #b3b3b3;
|
||||
--ion-color-step-350: #a6a6a6;
|
||||
--ion-color-step-400: #999999;
|
||||
--ion-color-step-450: #8c8c8c;
|
||||
--ion-color-step-500: #808080;
|
||||
--ion-color-step-550: #737373;
|
||||
--ion-color-step-600: #666666;
|
||||
--ion-color-step-650: #595959;
|
||||
--ion-color-step-700: #4d4d4d;
|
||||
--ion-color-step-750: #404040;
|
||||
--ion-color-step-800: #333333;
|
||||
--ion-color-step-850: #262626;
|
||||
--ion-color-step-900: #191919;
|
||||
--ion-color-step-950: #0d0d0d;
|
||||
|
||||
--alt-red: #ff4961;
|
||||
--alt-orange: #f89248;
|
||||
--alt-yellow: #e5d53e;
|
||||
--alt-green: #3dcf6f;
|
||||
--alt-blue: #00a8a8;
|
||||
--alt-purple: #9747ff;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
ViewEncapsulation,
|
||||
} from '@angular/core'
|
||||
import { AbstractTuiThemeSwitcher } from '@taiga-ui/cdk'
|
||||
|
||||
@Component({
|
||||
selector: 'light-theme',
|
||||
template: '',
|
||||
styleUrls: ['./light-theme.component.scss'],
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class LightThemeComponent extends AbstractTuiThemeSwitcher {}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
|
||||
import { LightThemeComponent } from './light-theme.component'
|
||||
|
||||
@NgModule({
|
||||
declarations: [LightThemeComponent],
|
||||
exports: [LightThemeComponent],
|
||||
})
|
||||
export class LightThemeModule {}
|
||||
6
frontend/projects/shared/src/tokens/theme.ts
Normal file
6
frontend/projects/shared/src/tokens/theme.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { InjectionToken } from '@angular/core'
|
||||
import { EMPTY, Observable } from 'rxjs'
|
||||
|
||||
export const THEME = new InjectionToken<Observable<string>>('App theme', {
|
||||
factory: () => EMPTY,
|
||||
})
|
||||
@@ -48,7 +48,7 @@
|
||||
--ion-color-light: #181818;
|
||||
--ion-color-light-rgb: 24, 24, 24;
|
||||
--ion-color-light-contrast: #ffffff;
|
||||
--ion-color-light-contrast-rgb: 0, 0, 0;
|
||||
--ion-color-light-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-light-shade: #000000;
|
||||
--ion-color-light-tint: #000000;
|
||||
|
||||
@@ -92,4 +92,4 @@
|
||||
--alt-green: #3DCF6F;
|
||||
--alt-blue: #00A8A8;
|
||||
--alt-purple: #9747FF;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<tui-root
|
||||
*ngIf="widgetDrawer$ | async as drawer"
|
||||
tuiMode="onDark"
|
||||
[tuiMode]="(theme$ | async) === 'Dark' ? 'onDark' : null"
|
||||
[style.--widgets-width.px]="drawer.open ? drawer.width : 0"
|
||||
>
|
||||
<ion-app appEnter>
|
||||
@@ -16,7 +16,7 @@
|
||||
side="start"
|
||||
class="left-menu"
|
||||
>
|
||||
<ion-content color="light" scrollY="false">
|
||||
<ion-content color="light" scrollY="false" class="menu">
|
||||
<app-menu *ngIf="authService.isVerified$ | async"></app-menu>
|
||||
</ion-content>
|
||||
</ion-menu>
|
||||
@@ -74,4 +74,13 @@
|
||||
<toast-container></toast-container>
|
||||
</ion-app>
|
||||
</tui-root>
|
||||
<tui-theme-night></tui-theme-night>
|
||||
<ng-container
|
||||
*ngIf="authService.isVerified$ | async"
|
||||
[ngSwitch]="theme$ | async"
|
||||
>
|
||||
<ng-container *ngSwitchCase="'Dark'">
|
||||
<tui-theme-night></tui-theme-night>
|
||||
<dark-theme></dark-theme>
|
||||
</ng-container>
|
||||
<light-theme *ngSwitchCase="'Light'"></light-theme>
|
||||
</ng-container>
|
||||
|
||||
@@ -11,6 +11,12 @@ tui-root {
|
||||
--side-max-width: 280px;
|
||||
}
|
||||
|
||||
.menu {
|
||||
:host-context(body[data-theme='Light']) & {
|
||||
--ion-color-base: #F4F4F5 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
transition: filter 0.3s;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, OnDestroy } from '@angular/core'
|
||||
import { Component, inject, OnDestroy } from '@angular/core'
|
||||
import { merge } from 'rxjs'
|
||||
import { AuthService } from './services/auth.service'
|
||||
import { SplitPaneTracker } from './services/split-pane.service'
|
||||
@@ -11,6 +11,8 @@ import {
|
||||
ClientStorageService,
|
||||
WidgetDrawer,
|
||||
} from './services/client-storage.service'
|
||||
import { ThemeSwitcherService } from './services/theme-switcher.service'
|
||||
import { THEME } from '@start9labs/shared'
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
@@ -21,6 +23,7 @@ export class AppComponent implements OnDestroy {
|
||||
readonly subscription = merge(this.patchData, this.patchMonitor).subscribe()
|
||||
readonly sidebarOpen$ = this.splitPane.sidebarOpen$
|
||||
readonly widgetDrawer$ = this.clientStorageService.widgetDrawer$
|
||||
readonly theme$ = inject(THEME)
|
||||
|
||||
constructor(
|
||||
private readonly titleService: Title,
|
||||
@@ -31,6 +34,7 @@ export class AppComponent implements OnDestroy {
|
||||
readonly authService: AuthService,
|
||||
readonly connection: ConnectionService,
|
||||
readonly clientStorageService: ClientStorageService,
|
||||
readonly themeSwitcher: ThemeSwitcherService,
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
|
||||
@@ -11,8 +11,10 @@ import { IonicModule } from '@ionic/angular'
|
||||
import { MonacoEditorModule } from '@materia-ui/ngx-monaco-editor'
|
||||
import {
|
||||
MarkdownModule,
|
||||
DarkThemeModule,
|
||||
ResponsiveColModule,
|
||||
SharedPipesModule,
|
||||
LightThemeModule,
|
||||
} from '@start9labs/shared'
|
||||
|
||||
import { AppComponent } from './app.component'
|
||||
@@ -58,6 +60,8 @@ import { WidgetsPageModule } from './pages/widgets/widgets.module'
|
||||
TuiThemeNightModule,
|
||||
WidgetsPageModule,
|
||||
ResponsiveColModule,
|
||||
DarkThemeModule,
|
||||
LightThemeModule,
|
||||
],
|
||||
providers: APP_PROVIDERS,
|
||||
bootstrap: [AppComponent],
|
||||
|
||||
@@ -2,13 +2,14 @@ import { APP_INITIALIZER, Provider } from '@angular/core'
|
||||
import { UntypedFormBuilder } from '@angular/forms'
|
||||
import { Router, RouteReuseStrategy } from '@angular/router'
|
||||
import { IonicRouteStrategy, IonNav } from '@ionic/angular'
|
||||
import { RELATIVE_URL, WorkspaceConfig } from '@start9labs/shared'
|
||||
import { RELATIVE_URL, THEME, WorkspaceConfig } from '@start9labs/shared'
|
||||
import { ApiService } from './services/api/embassy-api.service'
|
||||
import { MockApiService } from './services/api/embassy-mock-api.service'
|
||||
import { LiveApiService } from './services/api/embassy-live-api.service'
|
||||
import { AuthService } from './services/auth.service'
|
||||
import { ClientStorageService } from './services/client-storage.service'
|
||||
import { FilterPackagesPipe } from '../../../marketplace/src/pipes/filter-packages.pipe'
|
||||
import { ThemeSwitcherService } from './services/theme-switcher.service'
|
||||
|
||||
const {
|
||||
useMocks,
|
||||
@@ -37,6 +38,10 @@ export const APP_PROVIDERS: Provider[] = [
|
||||
provide: RELATIVE_URL,
|
||||
useValue: `/${api.url}/${api.version}`,
|
||||
},
|
||||
{
|
||||
provide: THEME,
|
||||
useExisting: ThemeSwitcherService,
|
||||
},
|
||||
]
|
||||
|
||||
export function appInitializer(
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
<a class="logo ion-padding" routerLink="/home">
|
||||
<img alt="Start9" src="assets/img/logo.png" />
|
||||
<img
|
||||
alt="Start9"
|
||||
src="assets/img/{{
|
||||
(theme$ | async) === 'Dark' ? 'logo' : 'logo_dark'
|
||||
}}.png"
|
||||
/>
|
||||
</a>
|
||||
<ion-item-group class="menu">
|
||||
<ion-menu-toggle *ngFor="let page of pages" auto-hide="false">
|
||||
<ion-item
|
||||
button
|
||||
class="link"
|
||||
routerLinkActive="link_selected"
|
||||
color="transparent"
|
||||
routerDirection="root"
|
||||
lines="none"
|
||||
@@ -45,7 +51,6 @@
|
||||
</ion-item>
|
||||
</ion-menu-toggle>
|
||||
</ion-item-group>
|
||||
|
||||
<img
|
||||
appSnek
|
||||
class="snek"
|
||||
|
||||
@@ -12,6 +12,15 @@
|
||||
padding: 30px 0;
|
||||
}
|
||||
|
||||
.link {
|
||||
--border-radius: 0;
|
||||
|
||||
:host-context(body[data-theme='Light']) &_selected {
|
||||
--ion-color-base: #333;
|
||||
--ion-color-contrast: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-left: 10px;
|
||||
}
|
||||
@@ -20,7 +29,7 @@
|
||||
color: var(--ion-color-dark-shade);
|
||||
|
||||
&_selected {
|
||||
color: #fff;
|
||||
color: var(--ion-color-dark);
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
inject,
|
||||
Inject,
|
||||
} from '@angular/core'
|
||||
import { EOSService } from '../../services/eos.service'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { combineLatest, filter, first, map, Observable, switchMap } from 'rxjs'
|
||||
@@ -6,7 +11,7 @@ import { AbstractMarketplaceService } from '@start9labs/marketplace'
|
||||
import { MarketplaceService } from 'src/app/services/marketplace.service'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { SplitPaneTracker } from 'src/app/services/split-pane.service'
|
||||
import { Emver } from '@start9labs/shared'
|
||||
import { Emver, THEME } from '@start9labs/shared'
|
||||
import { ConnectionService } from 'src/app/services/connection.service'
|
||||
|
||||
@Component({
|
||||
@@ -82,6 +87,8 @@ export class MenuComponent {
|
||||
|
||||
readonly sidebarOpen$ = this.splitPane.sidebarOpen$
|
||||
|
||||
readonly theme$ = inject(THEME)
|
||||
|
||||
constructor(
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
private readonly eosService: EOSService,
|
||||
|
||||
@@ -10,6 +10,7 @@ const ICONS = [
|
||||
'arrow-forward',
|
||||
'arrow-up',
|
||||
'briefcase-outline',
|
||||
'brush-outline',
|
||||
'bookmark-outline',
|
||||
'cellular-outline',
|
||||
'chatbubbles-outline',
|
||||
|
||||
@@ -4,11 +4,7 @@
|
||||
[color]="connection.color"
|
||||
>
|
||||
<div class="inline" slot="start">
|
||||
<ion-icon
|
||||
[name]="connection.icon"
|
||||
class="icon"
|
||||
[color]="connection.iconColor"
|
||||
></ion-icon>
|
||||
<ion-icon [name]="connection.icon" class="icon"></ion-icon>
|
||||
<p style="margin: 8px 0; font-weight: 600">{{ connection.message }}</p>
|
||||
<ion-spinner
|
||||
*ngIf="connection.dots"
|
||||
|
||||
@@ -15,7 +15,6 @@ export class ConnectionBarComponent {
|
||||
message: string
|
||||
color: string
|
||||
icon: string
|
||||
iconColor: string
|
||||
dots: boolean
|
||||
}> = combineLatest([
|
||||
this.connectionService.networkConnected$,
|
||||
@@ -27,7 +26,6 @@ export class ConnectionBarComponent {
|
||||
message: 'No Internet',
|
||||
color: 'danger',
|
||||
icon: 'cloud-offline-outline',
|
||||
iconColor: 'dark',
|
||||
dots: false,
|
||||
}
|
||||
if (!websocket)
|
||||
@@ -35,7 +33,6 @@ export class ConnectionBarComponent {
|
||||
message: 'Connecting',
|
||||
color: 'warning',
|
||||
icon: 'cloud-offline-outline',
|
||||
iconColor: 'light',
|
||||
dots: true,
|
||||
}
|
||||
|
||||
@@ -43,7 +40,6 @@ export class ConnectionBarComponent {
|
||||
message: 'Connected',
|
||||
color: 'success',
|
||||
icon: 'cloud-done',
|
||||
iconColor: 'light',
|
||||
dots: false,
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
}"
|
||||
></form-label>
|
||||
</h4>
|
||||
<ion-item color="dark">
|
||||
<ion-item [color]="(theme$ | async) === 'Light' ? 'light' : 'dark'">
|
||||
<ion-textarea
|
||||
*ngIf="spec.type === 'string' && spec.textarea; else notTextArea"
|
||||
[placeholder]="spec.placeholder || 'Enter ' + spec.name"
|
||||
@@ -308,7 +308,9 @@
|
||||
*ngIf="spec.subtype === 'string' || spec.subtype === 'number'"
|
||||
[id]="objectId | toElementId: entry.key:i"
|
||||
>
|
||||
<ion-item color="dark">
|
||||
<ion-item
|
||||
[color]="(theme$ | async) === 'Light' ? 'light' : 'dark'"
|
||||
>
|
||||
<ion-input
|
||||
type="text"
|
||||
[inputmode]="spec.subtype === 'number' ? 'tel' : 'text'"
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
EventEmitter,
|
||||
ChangeDetectionStrategy,
|
||||
Inject,
|
||||
inject,
|
||||
} from '@angular/core'
|
||||
import { FormArray, UntypedFormArray, UntypedFormGroup } from '@angular/forms'
|
||||
import { AlertButton, AlertController, ModalController } from '@ionic/angular'
|
||||
@@ -20,9 +21,10 @@ import {
|
||||
} from 'src/app/pkg-config/config-types'
|
||||
import { FormService } from 'src/app/services/form.service'
|
||||
import { EnumListPage } from 'src/app/modals/enum-list/enum-list.page'
|
||||
import { pauseFor } from '@start9labs/shared'
|
||||
import { THEME, pauseFor } from '@start9labs/shared'
|
||||
import { v4 } from 'uuid'
|
||||
import { DOCUMENT } from '@angular/common'
|
||||
|
||||
const Mustache = require('mustache')
|
||||
|
||||
interface Config {
|
||||
@@ -51,6 +53,8 @@ export class FormObjectComponent {
|
||||
} = {}
|
||||
objectId = v4()
|
||||
|
||||
readonly theme$ = inject(THEME)
|
||||
|
||||
constructor(
|
||||
private readonly alertCtrl: AlertController,
|
||||
private readonly modalCtrl: ModalController,
|
||||
|
||||
@@ -19,7 +19,10 @@
|
||||
<form (ngSubmit)="submit()">
|
||||
<div style="margin: 0 0 24px 16px">
|
||||
<p class="input-label">{{ options.label }}</p>
|
||||
<ion-item lines="none" color="dark">
|
||||
<ion-item
|
||||
lines="none"
|
||||
[color]="(theme$ | async) === 'Light' ? 'light' : 'dark'"
|
||||
>
|
||||
<ion-input
|
||||
#mainInput
|
||||
type="text"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Component, Input, ViewChild } from '@angular/core'
|
||||
import { Component, inject, Input, ViewChild } from '@angular/core'
|
||||
import { ModalController, IonicSafeString, IonInput } from '@ionic/angular'
|
||||
import { getErrorMessage } from '@start9labs/shared'
|
||||
import { getErrorMessage, THEME } from '@start9labs/shared'
|
||||
import { MaskPipe } from 'src/app/pipes/mask/mask.pipe'
|
||||
|
||||
@Component({
|
||||
@@ -21,6 +21,8 @@ export class GenericInputComponent {
|
||||
|
||||
error: string | IonicSafeString = ''
|
||||
|
||||
readonly theme$ = inject(THEME)
|
||||
|
||||
constructor(
|
||||
private readonly modalCtrl: ModalController,
|
||||
private readonly mask: MaskPipe,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
button
|
||||
*ngIf="pkg.entry.manifest as manifest"
|
||||
detail="false"
|
||||
class="service-card"
|
||||
[routerLink]="['/services', manifest.id]"
|
||||
>
|
||||
<app-list-icon slot="start" [pkg]="pkg"></app-list-icon>
|
||||
@@ -9,7 +10,7 @@
|
||||
<img alt="" [src]="pkg.entry['static-files'].icon" />
|
||||
</ion-thumbnail>
|
||||
<ion-label>
|
||||
<h2>{{ manifest.title }}</h2>
|
||||
<h2 ticker>{{ manifest.title }}</h2>
|
||||
<p>{{ manifest.version | displayEmver }}</p>
|
||||
<status
|
||||
[rendering]="pkg.primaryRendering"
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
EmverPipesModule,
|
||||
ResponsiveColModule,
|
||||
TextSpinnerComponentModule,
|
||||
TickerModule,
|
||||
} from '@start9labs/shared'
|
||||
import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module'
|
||||
import { StatusComponentModule } from 'src/app/components/status/status.component.module'
|
||||
@@ -37,6 +38,7 @@ const routes: Routes = [
|
||||
BadgeMenuComponentModule,
|
||||
WidgetListComponentModule,
|
||||
ResponsiveColModule,
|
||||
TickerModule,
|
||||
],
|
||||
declarations: [
|
||||
AppListPage,
|
||||
|
||||
@@ -3,10 +3,8 @@
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button defaultHref="services"></ion-back-button>
|
||||
</ion-buttons>
|
||||
<ion-item lines="none" color="light">
|
||||
<ion-thumbnail slot="start">
|
||||
<img [src]="pkg['static-files'].icon" alt="" />
|
||||
</ion-thumbnail>
|
||||
<div class="header">
|
||||
<img class="logo" [src]="pkg['static-files'].icon" alt="" />
|
||||
<ion-label>
|
||||
<h1
|
||||
class="montserrat"
|
||||
@@ -16,6 +14,6 @@
|
||||
</h1>
|
||||
<h2>{{ pkg.manifest.version | displayEmver }}</h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</div>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
.less-large {
|
||||
font-size: 18px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 54px;
|
||||
width: 54px;
|
||||
margin: 0 16px;
|
||||
}
|
||||
|
||||
@@ -18,8 +18,8 @@ const routes: Routes = [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
IonicModule,
|
||||
RouterModule.forChild(routes),
|
||||
SharedPipesModule,
|
||||
RouterModule.forChild(routes),
|
||||
],
|
||||
declarations: [LoginPage],
|
||||
})
|
||||
|
||||
@@ -1,41 +1,35 @@
|
||||
<ion-content>
|
||||
<ion-grid style="height: 100%; max-width: 540px">
|
||||
<ion-row class="ion-align-items-center" style="height: 90%">
|
||||
<ion-col class="ion-text-center">
|
||||
<div style="padding-bottom: 16px">
|
||||
<img src="assets/img/logo.png" style="max-width: 240px" />
|
||||
</div>
|
||||
<ion-content class="content">
|
||||
<ion-grid class="grid">
|
||||
<ion-row class="row">
|
||||
<ion-col>
|
||||
<img src="assets/img/logo.png" alt="Start9" class="logo" />
|
||||
|
||||
<ion-card>
|
||||
<ion-card-header class="ion-text-center" style="padding-bottom: 8px">
|
||||
<ion-card-title>Embassy Login</ion-card-title>
|
||||
<ion-card class="card">
|
||||
<ion-card-header>
|
||||
<ion-card-title class="title">Embassy Login</ion-card-title>
|
||||
</ion-card-header>
|
||||
|
||||
<ion-card-content class="ion-margin ion-text-center">
|
||||
<form
|
||||
class="inline"
|
||||
(submit)="submit()"
|
||||
style="margin-bottom: 12px"
|
||||
>
|
||||
<ion-card-content class="ion-margin">
|
||||
<form class="form" (submit)="submit()">
|
||||
<ion-item-group>
|
||||
<ion-item color="dark">
|
||||
<ion-item color="light">
|
||||
<ion-icon
|
||||
slot="start"
|
||||
name="key-outline"
|
||||
style="margin-right: 16px"
|
||||
></ion-icon>
|
||||
<ion-input
|
||||
[type]="unmasked ? 'text' : 'password'"
|
||||
name="password"
|
||||
placeholder="Password"
|
||||
[type]="unmasked ? 'text' : 'password'"
|
||||
[(ngModel)]="password"
|
||||
(ionChange)="error = ''"
|
||||
placeholder="Password"
|
||||
></ion-input>
|
||||
<ion-button fill="clear" color="light" (click)="toggleMask()">
|
||||
<ion-button fill="clear" color="dark" (click)="toggleMask()">
|
||||
<ion-icon
|
||||
slot="icon-only"
|
||||
[name]="unmasked ? 'eye-off-outline' : 'eye-outline'"
|
||||
size="small"
|
||||
[name]="unmasked ? 'eye-off-outline' : 'eye-outline'"
|
||||
></ion-icon>
|
||||
</ion-button>
|
||||
</ion-item>
|
||||
@@ -46,12 +40,12 @@
|
||||
expand="block"
|
||||
color="tertiary"
|
||||
>
|
||||
<span style="font-size: larger; font-weight: bold">Login</span>
|
||||
Login
|
||||
</ion-button>
|
||||
<p style="text-align: left; padding-top: 4px">
|
||||
<ion-text color="danger">{{ error }}</ion-text>
|
||||
</p>
|
||||
</form>
|
||||
<p class="error">
|
||||
<ion-text color="danger">{{ error }}</ion-text>
|
||||
</p>
|
||||
</ion-card-content>
|
||||
</ion-card>
|
||||
</ion-col>
|
||||
|
||||
@@ -1,10 +1,37 @@
|
||||
ion-card-title {
|
||||
margin: 24px 0;
|
||||
font-family: 'Montserrat';
|
||||
font-weight: 500;
|
||||
font-size: x-large;
|
||||
font-variant: all-small-caps;
|
||||
--color: var(--ion-color-dark);
|
||||
.content {
|
||||
--background: #222428;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #414141;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 24px 0 16px;
|
||||
color: #e0e0e0;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.grid {
|
||||
height: 100%;
|
||||
max-width: 540px;
|
||||
}
|
||||
|
||||
.row {
|
||||
height: 90%;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
max-width: 240px;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
.error {
|
||||
display: block;
|
||||
text-align: left;
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
ion-button {
|
||||
@@ -15,8 +42,7 @@ ion-item {
|
||||
--border-style: solid;
|
||||
--border-color: var(--ion-color-light);
|
||||
--border-radius: 4px 0 0 4px;
|
||||
box-shadow: 0 3px 1px -2px rgba(0, 0, 0, 0.2),
|
||||
0 2px 2px 0 rgba(0, 0, 0, 0.14),
|
||||
box-shadow: 0 3px 1px -2px rgba(0, 0, 0, 0.2), 0 2px 2px 0 rgba(0, 0, 0, 0.14),
|
||||
0 1px 5px 0 rgba(0, 0, 0, 0.12);
|
||||
|
||||
ion-button {
|
||||
@@ -26,26 +52,22 @@ ion-item {
|
||||
|
||||
ion-card {
|
||||
background: var(--ion-color-step-200);
|
||||
box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25);
|
||||
box-shadow: 0 4px 4px rgba(0, 0, 0, 0.25);
|
||||
border-radius: 44px;
|
||||
min-height: 16rem;
|
||||
}
|
||||
|
||||
.input-label {
|
||||
text-align: left;
|
||||
padding-bottom: 2px;
|
||||
font-size: small;
|
||||
font-weight: bold;
|
||||
color: var(--ion-color-dark);
|
||||
}
|
||||
|
||||
.login-button {
|
||||
margin-inline-start: 0;
|
||||
margin-inline-end: 0;
|
||||
height: 49px;
|
||||
font-size: larger;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.inline {
|
||||
.form {
|
||||
margin-bottom: 12px;
|
||||
|
||||
* {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
@@ -53,5 +75,5 @@ ion-card {
|
||||
}
|
||||
|
||||
.item-interactive {
|
||||
--highlight-background: var(--ion-color-tertiary) !important;
|
||||
}
|
||||
--highlight-background: #5260ff !important;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { TextSpinnerComponentModule } from '@start9labs/shared'
|
||||
import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module'
|
||||
import { OSUpdatePageModule } from 'src/app/modals/os-update/os-update.page.module'
|
||||
import { BackupColorPipeModule } from 'src/app/pipes/backup-color/backup-color.module'
|
||||
import { ThemeSwitcherModule } from '../theme-switcher/theme-switcher.module'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
@@ -26,6 +27,7 @@ const routes: Routes = [
|
||||
BadgeMenuComponentModule,
|
||||
OSUpdatePageModule,
|
||||
BackupColorPipeModule,
|
||||
ThemeSwitcherModule,
|
||||
],
|
||||
declarations: [ServerShowPage],
|
||||
})
|
||||
|
||||
@@ -38,65 +38,65 @@
|
||||
{{ cat.key }}
|
||||
</ion-text>
|
||||
</ion-item-divider>
|
||||
<ng-container *ngFor="let button of cat.value">
|
||||
<ion-item
|
||||
button
|
||||
[style.display]="(button.title === 'Repair Disk' && !(showDiskRepair$ | async)) ? 'none' : 'block'"
|
||||
[detail]="button.detail"
|
||||
[disabled]="button.disabled$ | async"
|
||||
(click)="button.action()"
|
||||
>
|
||||
<ion-icon slot="start" [name]="button.icon"></ion-icon>
|
||||
<ion-label>
|
||||
<h2>{{ button.title }}</h2>
|
||||
<p *ngIf="button.description">{{ button.description }}</p>
|
||||
<!-- <theme-switcher *ngIf="cat.key === 'Manage'"></theme-switcher> -->
|
||||
<ion-item
|
||||
*ngFor="let button of cat.value"
|
||||
button
|
||||
[style.display]="(button.title === 'Repair Disk' && !(showDiskRepair$ | async)) ? 'none' : 'block'"
|
||||
[detail]="button.detail"
|
||||
[disabled]="button.disabled$ | async"
|
||||
(click)="button.action()"
|
||||
>
|
||||
<ion-icon slot="start" [name]="button.icon"></ion-icon>
|
||||
<ion-label>
|
||||
<h2>{{ button.title }}</h2>
|
||||
<p *ngIf="button.description">{{ button.description }}</p>
|
||||
|
||||
<!-- "Create Backup" button only -->
|
||||
<p *ngIf="button.title === 'Create Backup'">
|
||||
<ng-container *ngIf="server['status-info'] as statusInfo">
|
||||
<ion-text
|
||||
[color]="server['last-backup'] | backupColor"
|
||||
*ngIf="!statusInfo['backup-progress'] && !statusInfo['update-progress']"
|
||||
>
|
||||
Last Backup: {{ server['last-backup'] ? (server['last-backup']
|
||||
| date: 'medium') : 'never' }}
|
||||
</ion-text>
|
||||
<span *ngIf="!!statusInfo['backup-progress']" class="inline">
|
||||
<ion-spinner
|
||||
color="success"
|
||||
style="height: 12px; width: 12px; margin-right: 6px"
|
||||
></ion-spinner>
|
||||
<ion-text color="success">Backing up</ion-text>
|
||||
</span>
|
||||
</ng-container>
|
||||
</p>
|
||||
<!-- "Software Update" button only -->
|
||||
<p *ngIf="button.title === 'Software Update'">
|
||||
<!-- "Create Backup" button only -->
|
||||
<p *ngIf="button.title === 'Create Backup'">
|
||||
<ng-container *ngIf="server['status-info'] as statusInfo">
|
||||
<ion-text
|
||||
*ngIf="server['status-info'].updated; else notUpdated"
|
||||
class="inline"
|
||||
color="warning"
|
||||
[color]="server['last-backup'] | backupColor"
|
||||
*ngIf="!statusInfo['backup-progress'] && !statusInfo['update-progress']"
|
||||
>
|
||||
Update Complete. Restart to apply changes
|
||||
Last Backup: {{ server['last-backup'] ? (server['last-backup'] |
|
||||
date: 'medium') : 'never' }}
|
||||
</ion-text>
|
||||
<ng-template #notUpdated>
|
||||
<ng-container *ngIf="showUpdate$ | async; else check">
|
||||
<ion-text class="inline" color="success">
|
||||
<ion-icon name="rocket-outline"></ion-icon>
|
||||
Update Available
|
||||
</ion-text>
|
||||
</ng-container>
|
||||
<ng-template #check>
|
||||
<ion-text class="inline" color="dark">
|
||||
<ion-icon name="refresh"></ion-icon>
|
||||
Check for updates
|
||||
</ion-text>
|
||||
</ng-template>
|
||||
<span *ngIf="!!statusInfo['backup-progress']" class="inline">
|
||||
<ion-spinner
|
||||
color="success"
|
||||
style="height: 12px; width: 12px; margin-right: 6px"
|
||||
></ion-spinner>
|
||||
<ion-text color="success">Backing up</ion-text>
|
||||
</span>
|
||||
</ng-container>
|
||||
</p>
|
||||
<!-- "Software Update" button only -->
|
||||
<p *ngIf="button.title === 'Software Update'">
|
||||
<ion-text
|
||||
*ngIf="server['status-info'].updated; else notUpdated"
|
||||
class="inline"
|
||||
color="warning"
|
||||
>
|
||||
Update Complete. Restart to apply changes
|
||||
</ion-text>
|
||||
<ng-template #notUpdated>
|
||||
<ng-container *ngIf="showUpdate$ | async; else check">
|
||||
<ion-text class="inline" color="success">
|
||||
<ion-icon name="rocket-outline"></ion-icon>
|
||||
Update Available
|
||||
</ion-text>
|
||||
</ng-container>
|
||||
<ng-template #check>
|
||||
<ion-text class="inline" color="dark">
|
||||
<ion-icon name="refresh"></ion-icon>
|
||||
Check for updates
|
||||
</ion-text>
|
||||
</ng-template>
|
||||
</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</div>
|
||||
</ion-item-group>
|
||||
</ion-content>
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
{{ uploadState?.message }}
|
||||
</h4>
|
||||
<div class="box" *ngIf="toUpload.icon && toUpload.manifest">
|
||||
<div class="service-card">
|
||||
<div class="card">
|
||||
<div class="row row_end">
|
||||
<ion-button
|
||||
style="
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
justify-content: space-evenly
|
||||
}
|
||||
|
||||
.service-card {
|
||||
.card {
|
||||
background: radial-gradient(var(--ion-color-step-100), transparent);
|
||||
min-width: 200px;
|
||||
max-width: 200px;
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
<ion-item button [detail]="true" (click)="open = true">
|
||||
<ion-icon slot="start" name="brush-outline"></ion-icon>
|
||||
<ion-label>
|
||||
<h2>Theme</h2>
|
||||
<p>{{ value }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ng-template
|
||||
[(tuiDialog)]="open"
|
||||
[tuiDialogOptions]="{
|
||||
label: 'Select theme',
|
||||
size: 's'
|
||||
}"
|
||||
>
|
||||
<tui-radio-list
|
||||
style="margin-top: 16px"
|
||||
size="l"
|
||||
[ngModel]="value"
|
||||
[items]="themes"
|
||||
(ngModelChange)="onChange($event)"
|
||||
></tui-radio-list>
|
||||
</ng-template>
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
||||
|
||||
import { ThemeSwitcherService } from '../../../services/theme-switcher.service'
|
||||
|
||||
@Component({
|
||||
selector: 'theme-switcher',
|
||||
templateUrl: './theme-switcher.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class ThemeSwitcherComponent {
|
||||
value = this.switcher.value
|
||||
|
||||
open = false
|
||||
|
||||
readonly themes = ['Dark', 'Light']
|
||||
|
||||
constructor(private readonly switcher: ThemeSwitcherService) {}
|
||||
|
||||
onChange(value: string): void {
|
||||
this.value = value
|
||||
this.switcher.next(value)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { TuiDialogModule } from '@taiga-ui/core'
|
||||
import { TuiRadioListModule } from '@taiga-ui/kit'
|
||||
|
||||
import { ThemeSwitcherComponent } from './theme-switcher.component'
|
||||
|
||||
@NgModule({
|
||||
imports: [IonicModule, FormsModule, TuiDialogModule, TuiRadioListModule],
|
||||
declarations: [ThemeSwitcherComponent],
|
||||
exports: [ThemeSwitcherComponent],
|
||||
})
|
||||
export class ThemeSwitcherModule {}
|
||||
@@ -57,7 +57,7 @@
|
||||
.content {
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
background: #333;
|
||||
background: var(--tui-base-02);
|
||||
border-radius: 24px;
|
||||
padding: 24px;
|
||||
box-sizing: border-box;
|
||||
|
||||
@@ -14,6 +14,7 @@ export const mockPatchData: DataModel = {
|
||||
ui: {
|
||||
name: `Matt's Embassy`,
|
||||
'ack-welcome': '1.0.0',
|
||||
theme: 'Dark',
|
||||
widgets: BUILT_IN_WIDGETS.filter(
|
||||
({ id }) =>
|
||||
id === 'favorites' ||
|
||||
|
||||
@@ -20,6 +20,7 @@ export interface UIData {
|
||||
}
|
||||
}
|
||||
'ack-instructions': Record<string, boolean>
|
||||
theme: string
|
||||
widgets: readonly Widget[]
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Inject, Injectable } from '@angular/core'
|
||||
import { WINDOW } from '@ng-web-apis/common'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { BehaviorSubject } from 'rxjs'
|
||||
import { ApiService } from './api/embassy-api.service'
|
||||
import { DataModel } from './patch-db/data-model'
|
||||
import { filter, take } from 'rxjs/operators'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class ThemeSwitcherService extends BehaviorSubject<string> {
|
||||
constructor(
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
private readonly embassyApi: ApiService,
|
||||
@Inject(WINDOW) private readonly windowRef: Window,
|
||||
) {
|
||||
super('Dark')
|
||||
|
||||
this.patch
|
||||
.watch$('ui', 'theme')
|
||||
.pipe(take(1), filter(Boolean))
|
||||
.subscribe(theme => {
|
||||
this.next(theme)
|
||||
})
|
||||
}
|
||||
|
||||
override next(currentTheme: string): void {
|
||||
this.embassyApi.setDbValue(['theme'], currentTheme)
|
||||
this.windowRef.document.body.setAttribute('data-theme', currentTheme)
|
||||
super.next(currentTheme)
|
||||
}
|
||||
}
|
||||
@@ -306,7 +306,22 @@ ion-footer.with-widgets {
|
||||
width: calc(100% - var(--widgets-width));
|
||||
}
|
||||
|
||||
body[data-theme='Light'] {
|
||||
.service-card {
|
||||
--background: #f4f4f5;
|
||||
--inner-border-width: 0;
|
||||
}
|
||||
|
||||
ion-header {
|
||||
box-shadow: 0 1px #e0e0e0;
|
||||
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
padding-left: 40px;
|
||||
list-style-type: disc;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user