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 @@
+
+
+
+
Dark theme
+
Dark theme setting will apply to this browser only.
+
+
+
+
+
+
+
+
+
+
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