diff --git a/src/App.vue b/src/App.vue index 80c94c8b..ed999d72 100644 --- a/src/App.vue +++ b/src/App.vue @@ -2,6 +2,16 @@ + + diff --git a/src/components/Breadcrumb.vue b/src/components/Breadcrumb.vue index fe82788e..5f9d0340 100644 --- a/src/components/Breadcrumb.vue +++ b/src/components/Breadcrumb.vue @@ -44,6 +44,7 @@ export default { diff --git a/src/components/BuildsFeedIndicator.vue b/src/components/BuildsFeedIndicator.vue index 42ddcdc0..90dfc88b 100644 --- a/src/components/BuildsFeedIndicator.vue +++ b/src/components/BuildsFeedIndicator.vue @@ -85,9 +85,12 @@ export default { diff --git a/src/components/CodeSnippet.vue b/src/components/CodeSnippet.vue index c4d34d68..f74d14fa 100644 --- a/src/components/CodeSnippet.vue +++ b/src/components/CodeSnippet.vue @@ -67,10 +67,13 @@ export default { diff --git a/src/components/DarkThemeControl.vue b/src/components/DarkThemeControl.vue new file mode 100644 index 00000000..98952555 --- /dev/null +++ b/src/components/DarkThemeControl.vue @@ -0,0 +1,59 @@ + + + + + diff --git a/src/components/Footer.vue b/src/components/Footer.vue index 328dc573..f35e9d0f 100644 --- a/src/components/Footer.vue +++ b/src/components/Footer.vue @@ -41,7 +41,9 @@ nav.nav-right { } nav a { - color: $color-text-secondary; + @include themed { + color: tget("color-text-secondary"); + } & + a { margin-left: 30px; @@ -50,7 +52,9 @@ nav a { nav a:hover, nav a:focus { - color: $color-text; outline: none; + @include themed { + color: tget("color-text"); + } } diff --git a/src/components/Header.vue b/src/components/Header.vue index 39743653..1b42c980 100644 --- a/src/components/Header.vue +++ b/src/components/Header.vue @@ -104,12 +104,18 @@ export default { .header { height: $header-height; box-shadow: 0 2px 4px 0 $border-color; - background-color: $header-color; align-items: center; box-sizing: border-box; display: flex; padding: 0 $header-padding-side; justify-content: space-between; + @include themed { + background-color: tget("header-color"); + } + @include themed-only(dark) { + background-color: tget("header-color"); + box-shadow: 0 2px 4px 0 #000000; + } &.loading:before { content: ""; @@ -194,11 +200,15 @@ export default { border: 1px solid rgba(255, 255, 255, 0.3); border-radius: 50%; color: #fff; - background-color: $header-color; + @include themed { + background-color: tget("header-color"); + } &.filled { - color: $header-color; background-color: #fff; + @include themed { + color: tget("header-color"); + } } svg { @@ -216,8 +226,10 @@ export default { background-color: rgba(255, 255, 255, 0.1); @include hf { - background-color: #fff; - color: $body-color; + @include themed { + background-color: tget("surface-color"); + color: tget("body-color"); + } } &:hover { @@ -228,6 +240,7 @@ export default { diff --git a/src/components/NoRecentBuilds.vue b/src/components/NoRecentBuilds.vue index 0a51f101..9ec138e9 100644 --- a/src/components/NoRecentBuilds.vue +++ b/src/components/NoRecentBuilds.vue @@ -18,7 +18,8 @@ export default { diff --git a/src/components/Notifications.vue b/src/components/Notifications.vue index d0eb38c8..b9bbb3ae 100644 --- a/src/components/Notifications.vue +++ b/src/components/Notifications.vue @@ -33,6 +33,7 @@ export default { diff --git a/src/components/Panel.vue b/src/components/Panel.vue index 86a36964..34bbdd66 100644 --- a/src/components/Panel.vue +++ b/src/components/Panel.vue @@ -21,13 +21,16 @@ export default { diff --git a/src/components/Popup.vue b/src/components/Popup.vue index 83d42291..16d5e2fc 100644 --- a/src/components/Popup.vue +++ b/src/components/Popup.vue @@ -44,11 +44,19 @@ export default { @import "../assets/styles/mixins"; .popup { position: absolute; - background: #fff; z-index: 5; border-radius: 3px; - box-shadow: 0 2px 4px 0 rgba(25, 45, 70, 0.05); - border: solid 1px #edeef1; + @include themed { + background: tget("surface-color"); + } + @include themed-only(default) { + border: solid 1px #edeef1; + box-shadow: 0 2px 4px 0 rgba(25, 45, 70, 0.05); + } + @include themed-only(dark) { + border: solid 1px tget("border-color"); + box-shadow: 0 2px 4px 0 #000000; + } &.popup.evict { transform: translateX(-9999px); } diff --git a/src/components/RepoItem.vue b/src/components/RepoItem.vue index 93d29f4f..74cb6e88 100644 --- a/src/components/RepoItem.vue +++ b/src/components/RepoItem.vue @@ -116,12 +116,19 @@ export default { .repo-item { border-radius: 4px; box-sizing: border-box; - box-shadow: 0 2px 4px 0 rgba($color-text, 0.1); - border: solid 1px $border-color; - background-color: #ffffff; - color: $color-text; padding: 15px; - transition: box-shadow linear 0.2s; + border: solid 1px $border-color; + transition: box-shadow linear 0.2s, background linear 0.2s; + @include themed { + color: tget("color-text"); + background: tget("surface-color"); + } + @include themed-only(default) { + box-shadow: 0 2px 4px 0 rgba(tget("color-text"), 0.1); + } + @include themed-only(dark) { + box-shadow: 0 2px 4px 0 rgba(tget("body-color"), 0.5); + } @include mobile(true) { padding: 10px; @@ -179,7 +186,9 @@ export default { height: 22px; font-size: 18px; line-height: normal; - color: $color-text; + @include themed { + color: tget("color-text"); + } display: flex; align-items: flex-start; @@ -214,16 +223,20 @@ export default { height: 15px; opacity: 0.2; border-bottom-left-radius: 8px; - border-left: solid 1px $color-text; - border-bottom: solid 1px $color-text; margin-left: 9px; + @include themed { + border-left: solid 1px tget("color-text"); + border-bottom: solid 1px tget("color-text"); + } } .icon-repository { width: 20px; height: 20px; margin-right: 10px; - color: $color-info; + @include themed { + color: tget("color-info"); + } flex-shrink: 0; } @@ -239,7 +252,9 @@ export default { display: flex; align-items: center; justify-content: space-between; - color: $color-text-secondary; + @include themed { + color: tget("color-text-secondary"); + } .header + & { margin-top: 6px; @@ -260,7 +275,9 @@ export default { text-overflow: ellipsis; white-space: nowrap; line-height: normal; - color: $color-text-secondary; + @include themed { + color: tget("color-text-secondary"); + } margin-right: 15px; .divider { @@ -305,9 +322,11 @@ export default { display: inline-block; width: 3px; height: 3px; - background: rgba($color-text, 0.25); border-radius: 50%; margin: 0 6px; + @include themed { + background: rgba(tget("color-text"), 0.25); + } } diff --git a/src/components/RepoItemLabel.vue b/src/components/RepoItemLabel.vue index 3fd18622..f45cc289 100644 --- a/src/components/RepoItemLabel.vue +++ b/src/components/RepoItemLabel.vue @@ -45,20 +45,16 @@ export default { } }, hrefPR() { - return this.build && this.repo && - `/link/${this.repo.slug}/tree/${this.build.ref}`; + return this.build && this.repo && `/link/${this.repo.slug}/tree/${this.build.ref}`; }, hrefTag() { - return this.build && this.repo && - `/link/${this.repo.slug}/tree/${this.build.ref}`; + return this.build && this.repo && `/link/${this.repo.slug}/tree/${this.build.ref}`; }, hrefBranch() { - return this.build && this.repo && - `/link/${this.repo.slug}/tree/refs/heads/${this.build.target}`; + return this.build && this.repo && `/link/${this.repo.slug}/tree/refs/heads/${this.build.target}`; }, hrefCommit() { - return this.build && this.repo && - `/link/${this.repo.slug}/commit/${this.build.after}`; + return this.build && this.repo && `/link/${this.repo.slug}/commit/${this.build.after}`; }, branch() { return this.build.target; @@ -95,16 +91,26 @@ export default { diff --git a/src/components/RepoLink.vue b/src/components/RepoLink.vue index cae0b5ec..d96686b2 100644 --- a/src/components/RepoLink.vue +++ b/src/components/RepoLink.vue @@ -33,6 +33,7 @@ export default { diff --git a/src/components/ReposPopup.vue b/src/components/ReposPopup.vue index 633443c5..06086ca4 100644 --- a/src/components/ReposPopup.vue +++ b/src/components/ReposPopup.vue @@ -97,21 +97,28 @@ export default { diff --git a/src/components/Search.vue b/src/components/Search.vue index 16bc5d18..df808bbc 100644 --- a/src/components/Search.vue +++ b/src/components/Search.vue @@ -131,6 +131,7 @@ export default { diff --git a/src/components/SystemAlert.vue b/src/components/SystemAlert.vue index a88e4243..1eb5ec50 100644 --- a/src/components/SystemAlert.vue +++ b/src/components/SystemAlert.vue @@ -4,9 +4,9 @@ diff --git a/src/components/UserMenu.vue b/src/components/UserMenu.vue index cbb77307..36e3ef1e 100644 --- a/src/components/UserMenu.vue +++ b/src/components/UserMenu.vue @@ -7,6 +7,7 @@ User settings + {{ $t("labels.logout") }} @@ -16,11 +17,13 @@ + + diff --git a/src/components/buttons/Button.vue b/src/components/buttons/Button.vue index 9c9a6b83..3016eb93 100644 --- a/src/components/buttons/Button.vue +++ b/src/components/buttons/Button.vue @@ -112,7 +112,9 @@ export default { } .button.bordered { - border: 1px solid rgba($color-text, 0.25); + @include themed { + border: 1px solid rgba(tget("color-text"), 0.25); + } } .button.bordered.size-m { @@ -124,27 +126,40 @@ export default { } .button.theme-default { - background-color: $color-text; + @include themed { + background-color: tget("color-text"); + } } .button.theme-light { - background-color: #fff; + @include themed { + background-color: #fff; + } } .button.theme-primary { - background-color: $color-primary; + @include themed { + background-color: tget("color-primary"); + } } .button.theme-danger { - background-color: #ff4164; + @include themed { + background-color: tget("color-danger"); + } } .button.outline { - background-color: transparent; + @include themed { + background-color: transparent; + } } .button.theme-default.outline { - color: $color-text; + @include themed { + background-color: transparent; + color: tget("color-text"); + } } .button.theme-light.outline { @@ -152,51 +167,71 @@ export default { } .button.theme-primary.outline { - color: $color-primary; + @include themed { + color: tget("color-primary"); + } } .button.theme-danger.outline { - color: #ff4164; + @include themed { + color: tget("color-danger"); + } } .button:focus { - outline: none; + @include themed { + outline: none; + } } .button.bordered:hover, .button.bordered:focus { - border-color: $color-primary; + @include themed { + border-color: tget("color-primary"); + } } .button.theme-default:focus, .button.theme-default:hover { - background-color: rgba($color-text, 0.8); + @include themed { + background-color: rgba(tget("color-text"), 0.8); + } } .button.theme-default.outline:focus, .button.theme-default.outline:hover { - color: $color-primary; - background-color: transparent; + @include themed { + background-color: transparent; + color: tget("color-primary"); + } } .button.theme-primary:focus, .button.theme-primary:hover { - background-color: #085cc1; + @include themed { + background-color: #085cc1; + } } .button.theme-primary.outline:focus, .button.theme-primary.outline:hover { - background-color: rgba(5, 100, 215, $button-outline-hover-bg-opacity); + @include themed { + background-color: rgba(5, 100, 215, tget("button-outline-hover-bg-opacity")); + } } .button.theme-danger:focus, .button.theme-danger:hover { - background-color: #dd3e60; + @include themed { + background-color: #dd3e60; + } } .button.theme-danger.outline:focus, .button.theme-danger.outline:hover { - background-color: rgba(255, 65, 100, $button-outline-hover-bg-opacity); + @include themed { + background-color: rgba(255, 65, 100, tget("button-outline-hover-bg-opacity")); + } } .button.loading { @@ -222,14 +257,19 @@ export default { } .button.theme-danger.outline.loading:before { - border-color: #dd3e60; + @include themed { + border-color: #dd3e60; + } } .button[disabled], .button:hover[disabled] { cursor: not-allowed; opacity: 0.25; - border-color: rgba($color-text, 0.5); - color: $color-text; + + @include themed { + border-color: rgba(tget("color-text"), 0.5); + color: tget("color-text"); + } } diff --git a/src/components/buttons/MoreButton.vue b/src/components/buttons/MoreButton.vue index bb0e99e0..3b73e7b8 100644 --- a/src/components/buttons/MoreButton.vue +++ b/src/components/buttons/MoreButton.vue @@ -20,6 +20,7 @@ export default { diff --git a/src/components/editable-list/EditableList.vue b/src/components/editable-list/EditableList.vue index 3e698f2f..0089170d 100644 --- a/src/components/editable-list/EditableList.vue +++ b/src/components/editable-list/EditableList.vue @@ -81,15 +81,18 @@ export default { diff --git a/src/components/forms/BaseCheckbox.vue b/src/components/forms/BaseCheckbox.vue index c40bf9ad..245ff9e3 100644 --- a/src/components/forms/BaseCheckbox.vue +++ b/src/components/forms/BaseCheckbox.vue @@ -25,6 +25,7 @@ export default { diff --git a/src/components/forms/BaseRadioButtons.vue b/src/components/forms/BaseRadioButtons.vue index e53c46e9..29c9ebbe 100644 --- a/src/components/forms/BaseRadioButtons.vue +++ b/src/components/forms/BaseRadioButtons.vue @@ -60,24 +60,32 @@ label:before { position: absolute; width: 18px; height: 18px; - border: 1px solid rgba($color-text, 0.25); border-radius: 50%; top: 0; left: 0; - background-color: #fff; + @include themed { + background-color: tget("surface-color"); + border: 1px solid rgba(tget("color-text"), 0.25); + } } label:hover:before { - border-color: $color-primary; + @include themed { + border-color: tget("color-primary"); + } } input:focus + label:before { - box-shadow: 0 0 4px 1px $color-primary; + @include themed { + box-shadow: 0 0 4px 1px tget("color-primary"); + } } input:checked + label:before { - background: $color-primary; border-color: transparent; + @include themed { + background: tget("color-primary"); + } } input:checked + label:after { diff --git a/src/components/forms/BaseSelect.vue b/src/components/forms/BaseSelect.vue index a83f457f..9107490e 100644 --- a/src/components/forms/BaseSelect.vue +++ b/src/components/forms/BaseSelect.vue @@ -16,10 +16,9 @@ export default { diff --git a/src/components/forms/BaseSwitch.vue b/src/components/forms/BaseSwitch.vue new file mode 100644 index 00000000..3d1a8e7b --- /dev/null +++ b/src/components/forms/BaseSwitch.vue @@ -0,0 +1,92 @@ + + + + + diff --git a/src/components/forms/BaseTextArea.vue b/src/components/forms/BaseTextArea.vue index 9c88c930..0fb2e891 100644 --- a/src/components/forms/BaseTextArea.vue +++ b/src/components/forms/BaseTextArea.vue @@ -13,10 +13,9 @@ export default { diff --git a/src/lib/theme.js b/src/lib/theme.js new file mode 100644 index 00000000..19fa2ee5 --- /dev/null +++ b/src/lib/theme.js @@ -0,0 +1,28 @@ +const localStorageKey = "theme"; +export const THEME = Object.freeze({ DEFAULT: "DEFAULT", DARK: "DARK" }); + +export function fetchSavedTheme() { + return localStorage.getItem(localStorageKey) || undefined; +} + +export function applyDarkTheme(enabled) { + const body = document.body; + body.classList.add("no-transition"); + + if (enabled) { + localStorage.setItem(localStorageKey, THEME.DARK); + body.classList.remove("theme--default"); + body.classList.add("theme--dark"); + } else { + localStorage.setItem(localStorageKey, THEME.DEFAULT); + body.classList.remove("theme--dark"); + body.classList.add("theme--default"); + } + + body.offsetHeight; // Trigger a reflow, flushing the CSS changes + body.classList.remove("no-transition"); +} + +export function applySavedTheme() { + applyDarkTheme(fetchSavedTheme() === THEME.DARK); +} diff --git a/src/store.js b/src/store.js index 1daf70dc..7ef7cf6c 100644 --- a/src/store.js +++ b/src/store.js @@ -492,7 +492,7 @@ export default new Vuex.Store({ insertBuildCollection(state.builds[slug].data, slug, build); } - updateBuildsFeedByBuild(state, build) + updateBuildsFeedByBuild(state, build); }, BUILDS_FEED_LOADING(state) { diff --git a/src/views/Account.vue b/src/views/Account.vue index 4e936844..cff5c027 100644 --- a/src/views/Account.vue +++ b/src/views/Account.vue @@ -80,6 +80,7 @@ export default { @@ -116,7 +119,9 @@ export default { } .empty-message { - color: $color-text-secondary; + @include themed { + color: tget("color-text-secondary"); + } } .build { @@ -130,7 +135,13 @@ export default { .build:hover .repo-item, .build:focus .repo-item { - box-shadow: 0 4px 10px 0 rgba($color-text, 0.25); + @include themed-only(dark) { + background: lighten(tget("surface-color"), 3%); + box-shadow: 0 4px 10px 0 darken(tget("body-color"), 20%); + } + @include themed-only(default) { + box-shadow: 0 4px 10px 0 rgba(tget("color-text"), 0.25); + } } .build + .build { @@ -139,7 +150,9 @@ export default { .loading { margin: 20px 0; - color: $color-text-secondary; + @include themed { + color: tget("color-text-secondary"); + } } .more-button { diff --git a/src/views/Login.vue b/src/views/Login.vue index ef0fa037..fd7b2f85 100644 --- a/src/views/Login.vue +++ b/src/views/Login.vue @@ -28,6 +28,7 @@ export default { diff --git a/src/views/Repo.vue b/src/views/Repo.vue index c179ae66..184778d5 100644 --- a/src/views/Repo.vue +++ b/src/views/Repo.vue @@ -133,7 +133,7 @@ export default { return (this.repo && this.repo.permissions && this.repo.permissions.write) || (this.user && this.user.admin); }, isAdmin() { - return (this.repo && this.repo.permissions && this.repo.permissions.admin); + return this.repo && this.repo.permissions && this.repo.permissions.admin; }, build() { const collection = this.$store.state.builds[this.slug]; @@ -193,10 +193,12 @@ h1 { } nav { - border-bottom: 1px solid $border-color; margin-bottom: 20px; padding-left: 15px; display: flex; + @include themed { + border-bottom: 1px solid tget("border-color"); + } svg { opacity: 0.6; @@ -210,7 +212,6 @@ nav { } nav a { - color: $color-text-secondary; height: 18px; line-height: 18px; padding-bottom: 11px; @@ -222,6 +223,9 @@ nav a { align-items: center; margin-right: 30px; border-bottom: 1px solid transparent; + @include themed { + color: tget("color-text-secondary"); + } @include mobile { letter-spacing: normal; @@ -231,12 +235,16 @@ nav a { } nav a.manually-active { - color: $color-text; + @include themed { + color: tget("color-text"); + } } nav a:hover, nav a:focus { - color: $color-text; + @include themed { + color: tget("color-text"); + } svg { opacity: 1; @@ -245,14 +253,18 @@ nav a:focus { nav a.manually-active:focus, nav a.manually-active:hover { - color: $color-primary; + @include themed { + color: tget("color-primary"); + } } nav a[disabled], nav a[disabled]:hover, nav a[disabled]:focus { pointer-events: none; - color: rgba($color-text, 0.25); + @include themed { + color: rgba(tget("color-text"), 0.25); + } } nav a svg { @@ -260,8 +272,10 @@ nav a svg { } nav .router-link-exact-active { - border-color: $color-text; - color: $color-text; + @include themed { + border-color: tget("color-text"); + color: tget("color-text"); + } } .fade-enter-active, @@ -290,7 +304,9 @@ nav .router-link-exact-active { max-width: 300px; line-height: 18px; display: block; - color: #2364d2; margin: 0px auto; + @include themed { + color: tget("color-primary"); + } } diff --git a/src/views/Settings.vue b/src/views/Settings.vue index bbb0e8b4..b830aaab 100644 --- a/src/views/Settings.vue +++ b/src/views/Settings.vue @@ -199,7 +199,6 @@ const timeouts = [15, 30, 60, 90, 120, 180, 240, 300, 360, 420, 480, 540, 600, 6