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

Merge pull request #1737 from weseek/feat/switch-color-scheme

Feat/switch color scheme
Yuki Takei 6 лет назад
Родитель
Сommit
ffea5b7ebf

+ 1 - 1
src/client/js/bootstrap.jsx

@@ -27,7 +27,7 @@ const websocketContainer = new WebsocketContainer(appContainer);
 
 logger.info('unstated containers have been initialized');
 
-appContainer.initPlugins();
+appContainer.init();
 appContainer.injectToWindow();
 
 /**

+ 4 - 0
src/client/js/components/Admin/ExportArchiveData/SelectCollectionsModal.jsx

@@ -192,12 +192,14 @@ class SelectCollectionsModal extends React.Component {
             </div>
             <div className="row mt-4">
               <div className="col-sm-12">
+                {/* FIXME: use something instead of <legend> */}
                 <legend>Page Collections</legend>
                 {this.renderGroups(GROUPS_PAGE)}
               </div>
             </div>
             <div className="row mt-4">
               <div className="col-sm-12">
+                {/* FIXME: use something instead of <legend> */}
                 <legend>User Collections</legend>
                 {this.renderGroups(GROUPS_USER, 'danger')}
                 {this.renderWarnForUser()}
@@ -205,12 +207,14 @@ class SelectCollectionsModal extends React.Component {
             </div>
             <div className="row mt-4">
               <div className="col-sm-12">
+                {/* FIXME: use something instead of <legend> */}
                 <legend>Config Collections</legend>
                 {this.renderGroups(GROUPS_CONFIG)}
               </div>
             </div>
             <div className="row mt-4">
               <div className="col-sm-12">
+                {/* FIXME: use something instead of <legend> */}
                 <legend>Other Collections</legend>
                 {this.renderOthers()}
               </div>

+ 1 - 0
src/client/js/components/Admin/ImportData/GrowiArchive/ImportForm.jsx

@@ -345,6 +345,7 @@ class ImportForm extends React.Component {
     return (
       <div className="mt-4 row">
         <div className="col-12">
+          {/* FIXME: use something instead of <legend> */}
           <legend>{groupName} Collections</legend>
           { wellContent != null && (
             <div className="card well small" role="alert">

+ 2 - 0
src/client/js/components/Admin/ImportDataPage.jsx

@@ -143,6 +143,7 @@ class ImportDataPage extends React.Component {
           role="form"
         >
           <fieldset>
+            {/* FIXME: use something instead of <legend> */}
             <legend>{t('admin:importer_management.import_from', { from: 'esa.io' })}</legend>
             <table className="table table-bordered table-mapping">
               <thead>
@@ -233,6 +234,7 @@ class ImportDataPage extends React.Component {
           role="form"
         >
           <fieldset>
+            {/* FIXME: use something instead of <legend> */}
             <legend>{t('admin:importer_management.import_from', { from: 'Qiita:Team' })}</legend>
             <table className="table table-bordered table-mapping">
               <thead>

+ 2 - 0
src/client/js/components/Admin/UserGroupDetail/UserGroupDetailPage.jsx

@@ -23,9 +23,11 @@ class UserGroupDetailPage extends React.Component {
         <div className="m-t-20 form-box">
           <UserGroupEditForm />
         </div>
+        {/* FIXME: use something instead of <legend> */}
         <legend className="m-t-20">{t('admin:user_group_management.user_list')}</legend>
         <UserGroupUserTable />
         <UserGroupUserModal />
+        {/* FIXME: use something instead of <legend> */}
         <legend className="m-t-20">{t('Page')}</legend>
         <div className="page-list">
           <UserGroupPageList />

+ 1 - 0
src/client/js/components/Admin/UserGroupDetail/UserGroupEditForm.jsx

@@ -63,6 +63,7 @@ class UserGroupEditForm extends React.Component {
     return (
       <form className="form-horizontal" onSubmit={this.handleSubmit}>
         <fieldset>
+          {/* FIXME: use something instead of <legend> */}
           <legend>{t('admin:user_group_management.basic_info')}</legend>
           <div className="form-group">
             <label htmlFor="name" className="col-sm-2 control-label">{t('Name')}</label>

+ 74 - 0
src/client/js/components/Navbar/PersonalDropdown.jsx

@@ -25,20 +25,94 @@ const PersonalDropdown = (props) => {
     window.location.href = '/logout';
   };
 
+  const followOsCheckboxModifiedHandler = (bool) => {
+    // reset user preference
+    if (bool) {
+      appContainer.setColorSchemePreference(null);
+    }
+    // set preferDarkModeByMediaQuery as users preference
+    else {
+      appContainer.setColorSchemePreference(appContainer.state.preferDarkModeByMediaQuery);
+    }
+  };
+
+  const userPreferenceSwitchModifiedHandler = (bool) => {
+    appContainer.setColorSchemePreference(bool);
+  };
+
+
+  /*
+   * render
+   */
+  const { preferDarkModeByMediaQuery, preferDarkModeByUser } = appContainer.state;
+  const isUserPreferenceExists = preferDarkModeByUser != null;
+  const isDarkMode = () => {
+    if (isUserPreferenceExists) {
+      return preferDarkModeByUser;
+    }
+    return preferDarkModeByMediaQuery;
+  };
+
   return (
     <>
+      {/* Button */}
       <a className="nav-link dropdown-toggle waves-effect waves-light" data-toggle="dropdown">
         <UserPicture user={user} withoutLink />&nbsp;{user.name}
       </a>
+
+      {/* Menu */}
       <div className="dropdown-menu dropdown-menu-right">
+
         <a className="dropdown-item" href={`/user/${user.username}`}><i className="icon-fw icon-user"></i>{ t('User\'s Home') }</a>
         <a className="dropdown-item" href="/me"><i className="icon-fw icon-wrench"></i>{ t('User Settings') }</a>
+
         <div className="dropdown-divider"></div>
+
         <a className="dropdown-item" href={`/user/${user.username}#user-draft-list`}><i className="icon-fw icon-docs"></i>{ t('List Drafts') }</a>
         <a className="dropdown-item" href="/trash"><i className="icon-fw icon-trash"></i>{ t('Deleted Pages') }</a>
+
+        <div className="dropdown-divider"></div>
+
+        <h6 className="dropdown-header">Color Scheme</h6>
+        <form className="px-4">
+          <div className="form-row align-items-center">
+            <div className="col-auto">
+              <div className="custom-control custom-checkbox">
+                <input
+                  id="cbFollowOs"
+                  className="custom-control-input"
+                  type="checkbox"
+                  checked={!isUserPreferenceExists}
+                  onChange={e => followOsCheckboxModifiedHandler(e.target.checked)}
+                />
+                <label className="custom-control-label" htmlFor="cbFollowOs">Use OS Setting</label>
+              </div>
+            </div>
+          </div>
+          <div className="form-row align-items-center">
+            <div className="col-auto d-flex">
+              <span className={isUserPreferenceExists ? '' : 'text-muted'}>Light</span>
+              <div className="custom-control custom-switch custom-checkbox-secondary ml-2">
+                <input
+                  id="swUserPreference"
+                  className="custom-control-input"
+                  type="checkbox"
+                  checked={isDarkMode()}
+                  disabled={!isUserPreferenceExists}
+                  onChange={e => userPreferenceSwitchModifiedHandler(e.target.checked)}
+                />
+                <label className="custom-control-label" htmlFor="swUserPreference"></label>
+              </div>
+              <span className={isUserPreferenceExists ? '' : 'text-muted'}>Dark</span>
+            </div>
+          </div>
+        </form>
+
         <div className="dropdown-divider"></div>
+
         <a className="dropdown-item" type="button" onClick={logoutHandler}><i className="icon-fw icon-power"></i>{ t('Sign out') }</a>
       </div>
+
     </>
   );
 

+ 68 - 0
src/client/js/services/AppContainer.js

@@ -31,6 +31,8 @@ export default class AppContainer extends Container {
 
     this.state = {
       editorMode: null,
+      preferDarkModeByMediaQuery: false,
+      preferDarkModeByUser: null,
     };
 
     const body = document.querySelector('body');
@@ -97,6 +99,33 @@ export default class AppContainer extends Container {
     return 'AppContainer';
   }
 
+  init() {
+    this.initColorScheme();
+    this.initPlugins();
+  }
+
+  async initColorScheme() {
+    const switchStateByMediaQuery = (mql) => {
+      const preferDarkMode = mql.matches;
+      this.setState({ preferDarkModeByMediaQuery: preferDarkMode });
+
+      this.applyColorScheme();
+    };
+
+    const mqlForDarkMode = window.matchMedia('(prefers-color-scheme: dark)');
+    // add event listener
+    mqlForDarkMode.addListener(switchStateByMediaQuery);
+
+    // restore settings from localStorage
+    const { localStorage } = window;
+    if (localStorage.preferDarkModeByUser != null) {
+      await this.setState({ preferDarkModeByUser: localStorage.preferDarkModeByUser === 'true' });
+    }
+
+    // initialize
+    switchStateByMediaQuery(mqlForDarkMode);
+  }
+
   initPlugins() {
     if (this.isPluginEnabled) {
       const growiPlugin = window.growiPlugin;
@@ -314,6 +343,45 @@ export default class AppContainer extends Container {
     targetComponent.launchDrawioModal(beginLineNumber, endLineNumber);
   }
 
+  /**
+   * Set color scheme preference by user
+   * @param {boolean} isDarkMode
+   */
+  async setColorSchemePreference(isDarkMode) {
+    await this.setState({ preferDarkModeByUser: isDarkMode });
+
+    // store settings to localStorage
+    const { localStorage } = window;
+    if (isDarkMode == null) {
+      delete localStorage.removeItem('preferDarkModeByUser');
+    }
+    else {
+      localStorage.preferDarkModeByUser = isDarkMode;
+    }
+
+    this.applyColorScheme();
+  }
+
+  /**
+   * Apply color scheme as 'dark' attribute of <html></html>
+   */
+  applyColorScheme() {
+    const { preferDarkModeByMediaQuery, preferDarkModeByUser } = this.state;
+    let isDarkMode = preferDarkModeByMediaQuery;
+    if (preferDarkModeByUser != null) {
+      isDarkMode = preferDarkModeByUser;
+    }
+
+    // switch to dark mode
+    if (isDarkMode) {
+      document.documentElement.setAttribute('dark', 'true');
+    }
+    // switch to light mode
+    else {
+      document.documentElement.removeAttribute('dark');
+    }
+  }
+
   async apiGet(path, params) {
     return this.apiRequest('get', path, { params });
   }

+ 0 - 22
src/client/styles/scss/_override-bootstrap-variables.scss

@@ -12,20 +12,6 @@ $warning: #ee773b;
 $danger: #ff0a54;
 $light: #dee2e6;
 $dark: #343a40;
-//## Gray and brand colors for use across Bootstrap.
-$gray-base:              #000 !default;
-$gray-darker:            lighten($gray-base, 13.5%); // #222
-$gray-dark:              lighten($gray-base, 20%);   // #333
-$gray:                   lighten($gray-base, 33.5%); // #555
-$gray-light:             lighten($gray-base, 46.7%); // #777
-$gray-lighter:           lighten($gray-base, 93.5%); // #eee
-$gray-extralight:        #fafafa; // Growi original
-
-$brand-primary:         $primary;
-$brand-success:         $success;
-$brand-info:            $info;
-$brand-warning:         $warning;
-$brand-danger:          $danger;
 
 //== Typography
 //
@@ -50,7 +36,6 @@ $border-radius-small: 0;
 //
 //## For each of Bootstrap's buttons, define text, background and border color.
 
-$btn-default-bg: $btn-default-bgcolor;
 $btn-border-radius: 0;
 $btn-border-radius-lg: 0;
 $btn-border-radius-sm: 0;
@@ -90,13 +75,6 @@ $modal-header-padding-x: 1rem;
 
 //== Alerts
 $alert-border-radius: 0;
-//** `<input>` background color
-$input-bg:                       $bodycolor;
-//** `<input disabled>` background color
-$input-bg-disabled:              $btn-default-bgcolor;
-
-//** `<input>` border color
-$input-border:                   $border;
 
 //== Progress bar
 $progress-height: 4px;

+ 20 - 14
src/client/styles/scss/theme/_apply-colors-dark.scss

@@ -26,7 +26,8 @@ input.form-control,
 textarea.form-control {
   color: lighten($color-global, 30%);
   background-color: darken($bgcolor-global, 5%);
-  border: 1px solid darken($border, 30%);
+  // FIXME: accent color
+  // border: 1px solid darken($border, 30%);
 }
 
 .form-control[disabled],
@@ -38,7 +39,8 @@ textarea.form-control {
 .input-group .input-group-addon {
   color: theme-color('dark');
   background-color: rgba($bgcolor-navbar, 0.4);
-  border: 1px solid darken($border, 30%);
+  // FIXME: accent color
+  // border: 1px solid darken($border, 30%);
   border-right: none;
 }
 
@@ -52,7 +54,8 @@ textarea.form-control {
   }
 
   .divider {
-    background-color: $border;
+    // FIXME:
+    // background-color: $border-color;
   }
 }
 
@@ -89,7 +92,8 @@ textarea.form-control {
 .table > tfoot > tr > td,
 .table > thead > tr > th,
 .table-bordered {
-  border-top: 1px solid $border;
+  // FIXME: use $table-dark-*
+  // border-top: 1px solid $border;
 }
 
 .table-bordered > thead > tr > th,
@@ -98,15 +102,18 @@ textarea.form-control {
 .table-bordered > thead > tr > td,
 .table-bordered > tbody > tr > td,
 .table-bordered > tfoot > tr > td {
-  border: 1px solid $border;
+  // FIXME: use $table-dark-*
+  // border: 1px solid $border;
 }
 
 .table > thead > tr > th {
-  border-bottom: 1px solid $border;
+  // FIXME: use $table-dark-*
+  // border-bottom: 1px solid $border;
 }
 
 .table-bordered {
-  border: 1px solid $border;
+  // FIXME: use $table-dark-*
+  // border: 1px solid $border;
 }
 
 /*
@@ -140,7 +147,8 @@ header.affix {
 .search-page {
   .input-group-btn {
     .btn-light {
-      border: 1px solid darken($border, 30%); // fit to input.form-control
+      // FIXME:
+      // border: 1px solid darken($border, 30%); // fit to input.form-control
     }
   }
 }
@@ -154,16 +162,14 @@ header.affix {
   }
 }
 
-legend {
-  border-color: lighten($border, 10%);
-}
-
 .wiki {
   h1 {
-    border-color: lighten($border, 10%);
+    // FIXME:
+    // border-color: lighten($border, 10%);
   }
   h2 {
-    border-color: $border;
+    // FIXME:
+    // border-color: $border;
   }
 }
 

+ 2 - 2
src/client/styles/scss/theme/_apply-colors-light.scss

@@ -59,10 +59,10 @@ header.affix {
 
 .wiki {
   h1 {
-    border-color: darken($border, 10%);
+    border-color: darken($border-color-theme, 10%);
   }
   h2 {
-    border-color: $border;
+    border-color: $border-color-theme;
   }
 }
 

+ 16 - 18
src/client/styles/scss/theme/_apply-colors.scss

@@ -1,8 +1,6 @@
 //
 //== Apply to Bootstrap
 //
-@import '~bootstrap/scss/bootstrap-reboot';
-
 @each $color, $value in $theme-colors {
   @include bg-variant('.bg-#{$color}', $value);
 }
@@ -22,7 +20,7 @@
 @each $theme-color, $color in $theme-colors {
   .custom-checkbox-#{$theme-color} {
     .custom-control-label::before {
-      border-color: #d7d7d7;
+      border-color: $input-border-color;
       transition: 0.3s ease-in-out;
     }
     .custom-control-input:checked + .custom-control-label::before {
@@ -30,29 +28,29 @@
       border-color: $color;
     }
     .custom-control-input:checked + .custom-control-label::after {
-      color: white;
+      color: $bgcolor-global;
     }
     .custom-control-input:not(:disabled):active ~ .custom-control-label::before {
-      color: white;
+      color: $bgcolor-global;
       background-color: $color;
       border-color: $color;
     }
     .custom-control-input:focus:not(:checked) ~ .custom-control-label::before {
-      color: white;
-      background-color: white;
-      border-color: #d7d7d7;
+      color: $bgcolor-global;
+      background-color: $bgcolor-global;
+      border-color: $input-focus-border-color;
     }
   }
 }
 
 @each $theme-color, $color in $theme-colors {
   .alert.alert-#{$theme-color} {
-    color: white;
+    color: $bgcolor-global;
     background: $color;
     border: none;
 
     a:not(.btn) {
-      color: white;
+      color: $bgcolor-global;
 
       &:hover,
       &:focus {
@@ -165,7 +163,7 @@ legend {
  */
 .modal {
   .modal-header {
-    border-bottom-color: $border;
+    border-bottom-color: $border-color-theme;
 
     &.bg-primary,
     &.bg-info,
@@ -193,7 +191,7 @@ legend {
   }
 
   .modal-footer {
-    border-top-color: $border;
+    border-top-color: $border-color-theme;
   }
 }
 
@@ -236,7 +234,7 @@ legend {
  */
 .crowi-sidebar {
   background-color: darken($bgcolor-global, 4%);
-  border-left: solid 1px $border;
+  border-left: solid 1px $border-color;
 
   .system-version {
     background-color: darken($bgcolor-global, 4%);
@@ -282,11 +280,11 @@ body.on-edit {
     background-color: darken($bgcolor-global, 2%);
 
     .page-editor-editor-container {
-      border-right-color: $navbar-border;
+      border-right-color: $border-color-theme;
 
       .navbar-editor {
         background-color: $bgcolor-global; // same color with active tab
-        border-bottom-color: $border;
+        border-bottom-color: $border-color-theme;
       }
     }
 
@@ -295,7 +293,7 @@ body.on-edit {
     }
 
     .page-editor-footer {
-      border-top-color: $border;
+      border-top-color: $border-color-theme;
     }
   }
 }
@@ -305,7 +303,7 @@ body.on-edit {
  */
 .growi .main {
   .page-comments-row {
-    border-top-color: $border;
+    border-top-color: $border-color-theme;
   }
 
   .page-comment .page-comment-main,
@@ -355,7 +353,7 @@ body.on-edit {
  */
 .page-attachments-row {
   background-color: darken($bgcolor-global, 2%);
-  border-top-color: $border;
+  border-top-color: $border-color-theme;
 }
 
 /*

+ 12 - 19
src/client/styles/scss/theme/default.scss

@@ -1,20 +1,20 @@
 @import '../variables';
+@import '../override-bootstrap-variables';
 
 // == Define colors
 //
 
 // colors for overriding bootstrap $theme-colors
 $primary: #112744;
-$secondary: #ced4da;
+// $secondary: #;
 // $info: #;
 // $success: #;
 // $warning: #;
 // $danger: #;
-$light: #f0f0f0;
+// $light: #;
 // $dark: #;
 
 // Background colors
-$bgcolor-global: white;
 $bgcolor-navbar: #334455;
 $bgcolor-inline-code: #f9f2f4;
 $bgcolor-card: #f5f5f5;
@@ -32,32 +32,25 @@ $color-inline-code: #c7254e;
 $fillcolor-logo-mark: lighten(desaturate($bgcolor-navbar, 10%), 15%);
 
 // Border colors
-$border: #f0f0f0;
-$navbar-border: #ccc;
-$active-navbar-border: lighten($navbar-border, 10%);
+$border-color-theme: #ccc; // former: `$navbar-border: #ccc;`
 
 // override bootstrap variables
 $link-color: $color-link;
 $link-hover-color: $color-link-hover;
 
-// light mode colors
-@media (prefers-color-scheme: no-preference),
-  (prefers-color-scheme: light) {}
-
-// dark mode colors
-@media (prefers-color-scheme: dark) {}
-
 //== Apply
 //
-@import 'apply-colors';
 
-// apply for no-preference or light mode
-@media (prefers-color-scheme: no-preference),
-  (prefers-color-scheme: light) {
+@import '~bootstrap/scss/bootstrap-reboot';
+
+html:not([dark]) {
+  $bgcolor-global: white;
+  @import 'apply-colors';
   @import 'apply-colors-light';
 }
 
-// apply for dark mode
-@media (prefers-color-scheme: dark) {
+html[dark] {
+  $bgcolor-global: black;
+  @import 'apply-colors';
   @import 'apply-colors-dark';
 }