Просмотр исходного кода

feat(admin-ui): Add dark mode theme & switcher component

Relates to #391
Michael Bromley 5 лет назад
Родитель
Сommit
76f80f64e4
26 измененных файлов с 185 добавлено и 25 удалено
  1. 14 14
      packages/admin-ui/i18n-coverage.json
  2. 13 2
      packages/admin-ui/src/lib/core/src/app.component.ts
  3. 22 3
      packages/admin-ui/src/lib/core/src/common/generated-types.ts
  4. 9 0
      packages/admin-ui/src/lib/core/src/components/theme-switcher/theme-switcher.component.html
  5. 36 0
      packages/admin-ui/src/lib/core/src/components/theme-switcher/theme-switcher.component.scss
  6. 28 0
      packages/admin-ui/src/lib/core/src/components/theme-switcher/theme-switcher.component.ts
  7. 4 1
      packages/admin-ui/src/lib/core/src/components/user-menu/user-menu.component.html
  8. 2 0
      packages/admin-ui/src/lib/core/src/core.module.ts
  9. 3 0
      packages/admin-ui/src/lib/core/src/data/client-state/client-defaults.ts
  10. 17 0
      packages/admin-ui/src/lib/core/src/data/client-state/client-resolvers.ts
  11. 3 1
      packages/admin-ui/src/lib/core/src/data/client-state/client-types.graphql
  12. 8 0
      packages/admin-ui/src/lib/core/src/data/definitions/client-definitions.ts
  13. 8 0
      packages/admin-ui/src/lib/core/src/data/providers/client-data.service.ts
  14. 1 0
      packages/admin-ui/src/lib/core/src/providers/local-storage/local-storage.service.ts
  15. 1 0
      packages/admin-ui/src/lib/core/src/public_api.ts
  16. 1 0
      packages/admin-ui/src/lib/static/i18n-messages/cs.json
  17. 1 0
      packages/admin-ui/src/lib/static/i18n-messages/de.json
  18. 1 0
      packages/admin-ui/src/lib/static/i18n-messages/en.json
  19. 2 1
      packages/admin-ui/src/lib/static/i18n-messages/es.json
  20. 1 0
      packages/admin-ui/src/lib/static/i18n-messages/fr.json
  21. 1 0
      packages/admin-ui/src/lib/static/i18n-messages/pl.json
  22. 1 0
      packages/admin-ui/src/lib/static/i18n-messages/pt_BR.json
  23. 1 0
      packages/admin-ui/src/lib/static/i18n-messages/zh_Hans.json
  24. 1 0
      packages/admin-ui/src/lib/static/i18n-messages/zh_Hant.json
  25. 4 2
      packages/admin-ui/src/lib/static/styles/theme/dark.scss
  26. 2 1
      packages/admin-ui/src/lib/static/styles/theme/default.scss

+ 14 - 14
packages/admin-ui/i18n-coverage.json

@@ -1,49 +1,49 @@
 {
-  "generatedOn": "2020-12-23T10:30:40.834Z",
-  "lastCommit": "52ecb3373341617646affa8b2060589cd4e4c36f",
+  "generatedOn": "2021-01-07T12:25:32.256Z",
+  "lastCommit": "733a1270b6b464048433f2a99a5be6c66f62903f",
   "translationStatus": {
     "cs": {
-      "tokenCount": 748,
+      "tokenCount": 749,
       "translatedCount": 687,
       "percentage": 92
     },
     "de": {
-      "tokenCount": 748,
+      "tokenCount": 749,
       "translatedCount": 596,
       "percentage": 80
     },
     "en": {
-      "tokenCount": 748,
-      "translatedCount": 747,
+      "tokenCount": 749,
+      "translatedCount": 748,
       "percentage": 100
     },
     "es": {
-      "tokenCount": 748,
-      "translatedCount": 455,
+      "tokenCount": 749,
+      "translatedCount": 458,
       "percentage": 61
     },
     "fr": {
-      "tokenCount": 748,
+      "tokenCount": 749,
       "translatedCount": 692,
-      "percentage": 93
+      "percentage": 92
     },
     "pl": {
-      "tokenCount": 748,
+      "tokenCount": 749,
       "translatedCount": 551,
       "percentage": 74
     },
     "pt_BR": {
-      "tokenCount": 748,
+      "tokenCount": 749,
       "translatedCount": 642,
       "percentage": 86
     },
     "zh_Hans": {
-      "tokenCount": 748,
+      "tokenCount": 749,
       "translatedCount": 533,
       "percentage": 71
     },
     "zh_Hant": {
-      "tokenCount": 748,
+      "tokenCount": 749,
       "translatedCount": 533,
       "percentage": 71
     }

+ 13 - 2
packages/admin-ui/src/lib/core/src/app.component.ts

@@ -1,4 +1,5 @@
-import { Component, OnInit } from '@angular/core';
+import { DOCUMENT } from '@angular/common';
+import { Component, HostBinding, Inject, OnInit } from '@angular/core';
 import { Observable } from 'rxjs';
 import { map } from 'rxjs/operators';
 
@@ -11,12 +12,22 @@ import { DataService } from './data/providers/data.service';
 })
 export class AppComponent implements OnInit {
     loading$: Observable<boolean>;
+    private _document?: Document;
 
-    constructor(private dataService: DataService) {}
+    constructor(private dataService: DataService, @Inject(DOCUMENT) private document?: any) {
+        this._document = document;
+    }
 
     ngOnInit() {
         this.loading$ = this.dataService.client
             .getNetworkStatus()
             .stream$.pipe(map(data => 0 < data.networkStatus.inFlightRequests));
+
+        this.dataService.client
+            .uiState()
+            .mapStream(data => data.uiState.theme)
+            .subscribe(theme => {
+                this._document?.body.setAttribute('data-theme', theme);
+            });
     }
 }

+ 22 - 3
packages/admin-ui/src/lib/core/src/common/generated-types.ts

@@ -422,7 +422,8 @@ export type Mutation = {
   setAsLoggedIn: UserStatus;
   setAsLoggedOut: UserStatus;
   setOrderCustomFields?: Maybe<Order>;
-  setUiLanguage?: Maybe<LanguageCode>;
+  setUiLanguage: LanguageCode;
+  setUiTheme: Scalars['String'];
   settlePayment: SettlePaymentResult;
   settleRefund: SettleRefundResult;
   transitionFulfillmentToState: TransitionFulfillmentToStateResult;
@@ -835,7 +836,12 @@ export type MutationSetOrderCustomFieldsArgs = {
 
 
 export type MutationSetUiLanguageArgs = {
-  languageCode?: Maybe<LanguageCode>;
+  languageCode: LanguageCode;
+};
+
+
+export type MutationSetUiThemeArgs = {
+  theme: Scalars['String'];
 };
 
 
@@ -4650,6 +4656,7 @@ export type UserStatus = {
 export type UiState = {
   __typename?: 'UiState';
   language: LanguageCode;
+  theme: Scalars['String'];
 };
 
 export type CurrentUserChannelInput = {
@@ -4912,6 +4919,13 @@ export type SetUiLanguageMutationVariables = Exact<{
 
 export type SetUiLanguageMutation = Pick<Mutation, 'setUiLanguage'>;
 
+export type SetUiThemeMutationVariables = Exact<{
+  theme: Scalars['String'];
+}>;
+
+
+export type SetUiThemeMutation = Pick<Mutation, 'setUiTheme'>;
+
 export type GetNetworkStatusQueryVariables = Exact<{ [key: string]: never; }>;
 
 
@@ -4933,7 +4947,7 @@ export type GetUiStateQueryVariables = Exact<{ [key: string]: never; }>;
 
 export type GetUiStateQuery = { uiState: (
     { __typename?: 'UiState' }
-    & Pick<UiState, 'language'>
+    & Pick<UiState, 'language' | 'theme'>
   ) };
 
 export type GetClientStateQueryVariables = Exact<{ [key: string]: never; }>;
@@ -7865,6 +7879,11 @@ export namespace SetUiLanguage {
   export type Mutation = SetUiLanguageMutation;
 }
 
+export namespace SetUiTheme {
+  export type Variables = SetUiThemeMutationVariables;
+  export type Mutation = SetUiThemeMutation;
+}
+
 export namespace GetNetworkStatus {
   export type Variables = GetNetworkStatusQueryVariables;
   export type Query = GetNetworkStatusQuery;

+ 9 - 0
packages/admin-ui/src/lib/core/src/components/theme-switcher/theme-switcher.component.html

@@ -0,0 +1,9 @@
+<button *ngIf="activeTheme$ | async as activeTheme" class="theme-toggle" (click)="toggleTheme(activeTheme)">
+    <span>{{ 'common.theme' | translate }}</span>
+    <div class="theme-icon default" [class.active]="activeTheme === 'default'">
+        <clr-icon shape="sun" class="is-solid"></clr-icon>
+    </div>
+    <div class="theme-icon dark" [class.active]="activeTheme === 'dark'">
+        <clr-icon shape="moon" class="is-solid"></clr-icon>
+    </div>
+</button>

+ 36 - 0
packages/admin-ui/src/lib/core/src/components/theme-switcher/theme-switcher.component.scss

@@ -0,0 +1,36 @@
+:host {
+    display: inline-flex;
+    justify-content: center;
+    align-items: center;
+}
+
+button.theme-toggle {
+    position: relative;
+    padding-left: 20px;
+    border: none;
+    background: transparent;
+    color: var(--clr-dropdown-item-color);
+    cursor: pointer;
+}
+
+.theme-icon {
+    position: absolute;
+    top: 0px;
+    left: 6px;
+    z-index: 0;
+    opacity: 0.2;
+    color: var(--color-text-300);
+    transition: opacity 0.3s, left 0.3s;
+
+    &.active {
+        z-index: 1;
+        left: 0px;
+        opacity: 1;
+    }
+    &.default.active {
+        color: #d6ae3f;
+    }
+    &.dark.active {
+        color: #ffdf3a;
+    }
+}

+ 28 - 0
packages/admin-ui/src/lib/core/src/components/theme-switcher/theme-switcher.component.ts

@@ -0,0 +1,28 @@
+import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
+import { Observable } from 'rxjs';
+
+import { DataService } from '../../data/providers/data.service';
+import { LocalStorageService } from '../../providers/local-storage/local-storage.service';
+
+@Component({
+    selector: 'vdr-theme-switcher',
+    templateUrl: './theme-switcher.component.html',
+    styleUrls: ['./theme-switcher.component.scss'],
+    changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class ThemeSwitcherComponent implements OnInit {
+    activeTheme$: Observable<string>;
+
+    constructor(private dataService: DataService, private localStorageService: LocalStorageService) {}
+
+    ngOnInit() {
+        this.activeTheme$ = this.dataService.client.uiState().mapStream(data => data.uiState.theme);
+    }
+
+    toggleTheme(current: string) {
+        const newTheme = current === 'default' ? 'dark' : 'default';
+        this.dataService.client.setUiTheme(newTheme).subscribe(() => {
+            this.localStorageService.set('activeTheme', newTheme);
+        });
+    }
+}

+ 4 - 1
packages/admin-ui/src/lib/core/src/components/user-menu/user-menu.component.html

@@ -17,8 +17,11 @@
             >
                 <clr-icon shape="language"></clr-icon> {{ 'lang.' + uiLanguage | translate }}
             </button>
-            <div class="dropdown-divider"></div>
         </ng-container>
+        <div class="dropdown-item">
+            <vdr-theme-switcher></vdr-theme-switcher>
+        </div>
+        <div class="dropdown-divider"></div>
         <button type="button" vdrDropdownItem (click)="logOut.emit()">
             <clr-icon shape="logout"></clr-icon> {{ 'common.log-out' | translate }}
         </button>

+ 2 - 0
packages/admin-ui/src/lib/core/src/core.module.ts

@@ -14,6 +14,7 @@ import { ChannelSwitcherComponent } from './components/channel-switcher/channel-
 import { MainNavComponent } from './components/main-nav/main-nav.component';
 import { NotificationComponent } from './components/notification/notification.component';
 import { OverlayHostComponent } from './components/overlay-host/overlay-host.component';
+import { ThemeSwitcherComponent } from './components/theme-switcher/theme-switcher.component';
 import { UiLanguageSwitcherDialogComponent } from './components/ui-language-switcher-dialog/ui-language-switcher-dialog.component';
 import { UserMenuComponent } from './components/user-menu/user-menu.component';
 import { DataModule } from './data/data.module';
@@ -54,6 +55,7 @@ import { SharedModule } from './shared/shared.module';
         NotificationComponent,
         UiLanguageSwitcherDialogComponent,
         ChannelSwitcherComponent,
+        ThemeSwitcherComponent,
     ],
 })
 export class CoreModule {

+ 3 - 0
packages/admin-ui/src/lib/core/src/data/client-state/client-defaults.ts

@@ -1,9 +1,11 @@
+import { getAppConfig } from '../../app.config';
 import { GetNetworkStatus, GetUiState, GetUserStatus } from '../../common/generated-types';
 import { getDefaultUiLanguage } from '../../common/utilities/get-default-ui-language';
 import { LocalStorageService } from '../../providers/local-storage/local-storage.service';
 
 export function getClientDefaults(localStorageService: LocalStorageService) {
     const currentLanguage = localStorageService.get('uiLanguageCode') || getDefaultUiLanguage();
+    const activeTheme = localStorageService.get('activeTheme') || 'default';
     return {
         networkStatus: {
             inFlightRequests: 0,
@@ -20,6 +22,7 @@ export function getClientDefaults(localStorageService: LocalStorageService) {
         } as GetUserStatus.UserStatus,
         uiState: {
             language: currentLanguage,
+            theme: activeTheme,
             __typename: 'UiState',
         } as GetUiState.UiState,
     };

+ 17 - 0
packages/admin-ui/src/lib/core/src/data/client-state/client-resolvers.ts

@@ -8,6 +8,7 @@ import {
     SetActiveChannel,
     SetAsLoggedIn,
     SetUiLanguage,
+    SetUiTheme,
     UpdateUserChannels,
     UserStatus,
 } from '../../common/generated-types';
@@ -69,15 +70,31 @@ export const clientResolvers: ResolverDefinition = {
             return data.userStatus;
         },
         setUiLanguage: (_, args: SetUiLanguage.Variables, { cache }): LanguageCode => {
+            // tslint:disable-next-line:no-non-null-assertion
+            const previous = cache.readQuery<GetUiState.Query>({ query: GET_UI_STATE })!;
             const data: GetUiState.Query = {
                 uiState: {
                     __typename: 'UiState',
                     language: args.languageCode,
+                    theme: previous.uiState.theme,
                 },
             };
             cache.writeQuery({ query: GET_UI_STATE, data });
             return args.languageCode;
         },
+        setUiTheme: (_, args: SetUiTheme.Variables, { cache }): string => {
+            // tslint:disable-next-line:no-non-null-assertion
+            const previous = cache.readQuery<GetUiState.Query>({ query: GET_UI_STATE })!;
+            const data: GetUiState.Query = {
+                uiState: {
+                    __typename: 'UiState',
+                    language: previous.uiState.language,
+                    theme: args.theme,
+                },
+            };
+            cache.writeQuery({ query: GET_UI_STATE, data });
+            return args.theme;
+        },
         setActiveChannel: (_, args: SetActiveChannel.Variables, { cache }): UserStatus => {
             // tslint:disable-next-line:no-non-null-assertion
             const previous = cache.readQuery<GetUserStatus.Query>({ query: GET_USER_STATUS })!;

+ 3 - 1
packages/admin-ui/src/lib/core/src/data/client-state/client-types.graphql

@@ -9,7 +9,8 @@ type Mutation {
     requestCompleted: Int!
     setAsLoggedIn(input: UserStatusInput!): UserStatus!
     setAsLoggedOut: UserStatus!
-    setUiLanguage(languageCode: LanguageCode): LanguageCode
+    setUiLanguage(languageCode: LanguageCode!): LanguageCode!
+    setUiTheme(theme: String!): String!
     setActiveChannel(channelId: ID!): UserStatus!
     updateUserChannels(channels: [CurrentUserChannelInput!]!): UserStatus!
 }
@@ -29,6 +30,7 @@ type UserStatus {
 
 type UiState {
     language: LanguageCode!
+    theme: String!
 }
 
 input CurrentUserChannelInput {

+ 8 - 0
packages/admin-ui/src/lib/core/src/data/definitions/client-definitions.ts

@@ -52,6 +52,12 @@ export const SET_UI_LANGUAGE = gql`
     }
 `;
 
+export const SET_UI_THEME = gql`
+    mutation SetUiTheme($theme: String!) {
+        setUiTheme(theme: $theme) @client
+    }
+`;
+
 export const GET_NEWTORK_STATUS = gql`
     query GetNetworkStatus {
         networkStatus @client {
@@ -73,6 +79,7 @@ export const GET_UI_STATE = gql`
     query GetUiState {
         uiState @client {
             language
+            theme
         }
     }
 `;
@@ -87,6 +94,7 @@ export const GET_CLIENT_STATE = gql`
         }
         uiState @client {
             language
+            theme
         }
     }
     ${USER_STATUS_FRAGMENT}

+ 8 - 0
packages/admin-ui/src/lib/core/src/data/providers/client-data.service.ts

@@ -10,6 +10,7 @@ import {
     SetActiveChannel,
     SetAsLoggedIn,
     SetUiLanguage,
+    SetUiTheme,
     UpdateUserChannels,
 } from '../../common/generated-types';
 import {
@@ -22,6 +23,7 @@ import {
     SET_AS_LOGGED_IN,
     SET_AS_LOGGED_OUT,
     SET_UI_LANGUAGE,
+    SET_UI_THEME,
     UPDATE_USER_CHANNELS,
 } from '../definitions/client-definitions';
 
@@ -78,6 +80,12 @@ export class ClientDataService {
         });
     }
 
+    setUiTheme(theme: string) {
+        return this.baseDataService.mutate<SetUiTheme.Mutation, SetUiTheme.Variables>(SET_UI_THEME, {
+            theme,
+        });
+    }
+
     setActiveChannel(channelId: string) {
         return this.baseDataService.mutate<SetActiveChannel.Mutation, SetActiveChannel.Variables>(
             SET_ACTIVE_CHANNEL,

+ 1 - 0
packages/admin-ui/src/lib/core/src/providers/local-storage/local-storage.service.ts

@@ -10,6 +10,7 @@ export type LocalStorageTypeMap = {
     uiLanguageCode: LanguageCode;
     orderListLastCustomFilters: any;
     dashboardWidgetLayout: WidgetLayoutDefinition;
+    activeTheme: string;
 };
 
 export type LocalStorageLocationBasedTypeMap = {

+ 1 - 0
packages/admin-ui/src/lib/core/src/public_api.ts

@@ -25,6 +25,7 @@ export * from './components/channel-switcher/channel-switcher.component';
 export * from './components/main-nav/main-nav.component';
 export * from './components/notification/notification.component';
 export * from './components/overlay-host/overlay-host.component';
+export * from './components/theme-switcher/theme-switcher.component';
 export * from './components/ui-language-switcher-dialog/ui-language-switcher-dialog.component';
 export * from './components/user-menu/user-menu.component';
 export * from './core.module';

+ 1 - 0
packages/admin-ui/src/lib/static/i18n-messages/cs.json

@@ -218,6 +218,7 @@
     "select": "Vybrat...",
     "select-display-language": "Vyberte jazyk",
     "select-today": "Vybrat dnešní datum",
+    "theme": "",
     "there-are-unsaved-changes": "Provedené změny nebyly uloženy. Přechod na jinou stránku způsobí ztrátu těchto změn.",
     "toggle-all": "Přepnout vše",
     "update": "Aktualizovat",

+ 1 - 0
packages/admin-ui/src/lib/static/i18n-messages/de.json

@@ -218,6 +218,7 @@
     "select": "Auswählen...",
     "select-display-language": "Anzeigesprache wählen",
     "select-today": "Heute auswählen",
+    "theme": "",
     "there-are-unsaved-changes": "Es gibt ungespeicherte Änderungen. Wenn Sie wechseln, gehen diese Änderungen verloren.",
     "toggle-all": "",
     "update": "Aktualisieren",

+ 1 - 0
packages/admin-ui/src/lib/static/i18n-messages/en.json

@@ -218,6 +218,7 @@
     "select": "Select...",
     "select-display-language": "Select display language",
     "select-today": "Select today",
+    "theme": "Theme",
     "there-are-unsaved-changes": "There are unsaved changes. Navigating away will cause these changes to be lost.",
     "toggle-all": "Toggle all",
     "update": "Update",

+ 2 - 1
packages/admin-ui/src/lib/static/i18n-messages/es.json

@@ -218,6 +218,7 @@
     "select": "Seleccionar...",
     "select-display-language": "Seleccionar idioma de interfaz",
     "select-today": "Hoy",
+    "theme": "",
     "there-are-unsaved-changes": "Hay cambios sin guardar. Si sale de este sitio sus cambios se perderán.",
     "toggle-all": "",
     "update": "Actualizar",
@@ -781,4 +782,4 @@
     "job-result": "Resultado",
     "job-state": "Estado"
   }
-}
+}

+ 1 - 0
packages/admin-ui/src/lib/static/i18n-messages/fr.json

@@ -218,6 +218,7 @@
     "select": "Selectionner...",
     "select-display-language": "Choisir la langue d'affichage",
     "select-today": "Choisir aujourd'hui",
+    "theme": "",
     "there-are-unsaved-changes": "Il y a des changements non enregistrés. Naviguer ailleurs fera perdre ces changements.",
     "toggle-all": "Cocher/décocher Tout",
     "update": "Mettre à jour",

+ 1 - 0
packages/admin-ui/src/lib/static/i18n-messages/pl.json

@@ -218,6 +218,7 @@
     "select": "Wybrano...",
     "select-display-language": "Wybierz język",
     "select-today": "Wybierz dzisiaj",
+    "theme": "",
     "there-are-unsaved-changes": "Są nie zapisane zmiany. Nawigacja do innej lokalizacji spowoduje utrate zmian.",
     "toggle-all": "",
     "update": "Aktualizuj",

+ 1 - 0
packages/admin-ui/src/lib/static/i18n-messages/pt_BR.json

@@ -218,6 +218,7 @@
     "select": "Selecione...",
     "select-display-language": "Selecionar idioma de exibição",
     "select-today": "Selecione hoje",
+    "theme": "",
     "there-are-unsaved-changes": "Há alterações não salvas. Navegar para outra página fará com que essas alterações sejam perdidas.",
     "toggle-all": "",
     "update": "Atualização",

+ 1 - 0
packages/admin-ui/src/lib/static/i18n-messages/zh_Hans.json

@@ -218,6 +218,7 @@
     "select": "选择...",
     "select-display-language": "选择显示语言",
     "select-today": "选择今天",
+    "theme": "",
     "there-are-unsaved-changes": "修改尚未被保存,现在离开会导致您的修改会被删除",
     "toggle-all": "",
     "update": "确认修改",

+ 1 - 0
packages/admin-ui/src/lib/static/i18n-messages/zh_Hant.json

@@ -218,6 +218,7 @@
     "select": "選擇...",
     "select-display-language": "選擇顯示語言",
     "select-today": "選擇今天",
+    "theme": "",
     "there-are-unsaved-changes": "變更尚未被儲存,離開會失去所有變更",
     "toggle-all": "",
     "update": "確認修改",

+ 4 - 2
packages/admin-ui/src/lib/static/styles/theme/dark.scss

@@ -16,6 +16,8 @@
     --color-component-border-100: var(--color-grey-700);
     --color-component-border-200: var(--color-grey-600);
     --color-component-border-300: var(--color-grey-500);
+    --color-text-100: hsl(210, 16%, 93%);
+    --color-text-200: hsl(203, 16%, 72%);
     --color-text-300: var(--color-grey-300);
 
     --color-chip-warning-border: var(--color-warning-700);
@@ -539,8 +541,8 @@
     /**********
     * Typography
     */
-    --clr-global-font-color: hsl(210, 16%, 93%);
-    --clr-global-font-color-secondary: hsl(203, 16%, 72%);
+    --clr-global-font-color: var(--color-text-100);
+    --clr-global-font-color-secondary: var(--color-text-200);
 
     --clr-h1-color: var(--clr-global-font-color);
     --clr-h2-color: var(--clr-global-font-color);

+ 2 - 1
packages/admin-ui/src/lib/static/styles/theme/default.scss

@@ -1,6 +1,7 @@
 @import "variables";
 
-// Default Vendure light theme
+// Default Vendure light theme. The Clarity component custom properties
+// are left as their defaults.
 :root {
     // Colors
     --color-primary-100: #{$color-primary-100};