mirror of
https://github.com/woodpecker-ci/woodpecker.git
synced 2025-01-04 22:58:43 +00:00
Refactor UI dark/bright mode (#2590)
- collapse pipeline configs by default, closes https://github.com/woodpecker-ci/woodpecker/discussions/2557 - refactor ui themes: instead of just differentiating between "is dark" or "is not dark", add a third "auto" option following the browser settings (closes https://github.com/woodpecker-ci/woodpecker/discussions/2204) and put everything into an enum. also move the option from the navbar to user settings. --------- Co-authored-by: Anbraten <anton@ju60.de>
This commit is contained in:
parent
4198c447fb
commit
2ff916c804
8 changed files with 56 additions and 62 deletions
|
@ -12,8 +12,6 @@
|
||||||
"password": "Password",
|
"password": "Password",
|
||||||
"url": "URL",
|
"url": "URL",
|
||||||
"back": "Back",
|
"back": "Back",
|
||||||
"color_scheme_light": "Switch to dark mode",
|
|
||||||
"color_scheme_dark": "Switch to light mode",
|
|
||||||
"unknown_error": "An unknown error occurred",
|
"unknown_error": "An unknown error occurred",
|
||||||
"documentation_for": "Documentation for \"{topic}\"",
|
"documentation_for": "Documentation for \"{topic}\"",
|
||||||
"pipeline_feed": "Pipeline feed",
|
"pipeline_feed": "Pipeline feed",
|
||||||
|
@ -453,7 +451,13 @@
|
||||||
"settings": "User Settings",
|
"settings": "User Settings",
|
||||||
"general": {
|
"general": {
|
||||||
"general": "General",
|
"general": "General",
|
||||||
"language": "Language"
|
"language": "Language",
|
||||||
|
"theme": {
|
||||||
|
"theme": "Theme",
|
||||||
|
"light": "Light",
|
||||||
|
"dark": "Dark",
|
||||||
|
"auto": "Auto"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"secrets": {
|
"secrets": {
|
||||||
"secrets": "Secrets",
|
"secrets": "Secrets",
|
||||||
|
|
|
@ -39,13 +39,14 @@ import Icon from '~/components/atomic/Icon.vue';
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
title?: string;
|
title?: string;
|
||||||
collapsable?: boolean;
|
collapsable?: boolean;
|
||||||
|
collapsedByDefault?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* _collapsed is used to store the internal state of the panel, but is
|
* _collapsed is used to store the internal state of the panel, but is
|
||||||
* ignored if the panel is not collapsable.
|
* ignored if the panel is not collapsable.
|
||||||
*/
|
*/
|
||||||
const _collapsed = ref(false);
|
const _collapsed = ref(props.collapsedByDefault || false);
|
||||||
|
|
||||||
const collapsed = computed(() => props.collapsable && _collapsed.value);
|
const collapsed = computed(() => props.collapsable && _collapsed.value);
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -26,13 +26,6 @@
|
||||||
</div>
|
</div>
|
||||||
<!-- Right Icons Box -->
|
<!-- Right Icons Box -->
|
||||||
<div class="flex ml-auto -m-1.5 items-center space-x-2">
|
<div class="flex ml-auto -m-1.5 items-center space-x-2">
|
||||||
<!-- Dark Mode Toggle -->
|
|
||||||
<IconButton
|
|
||||||
:icon="darkMode ? 'dark' : 'light'"
|
|
||||||
:title="$t(darkMode ? 'color_scheme_dark' : 'color_scheme_light')"
|
|
||||||
class="navbar-icon"
|
|
||||||
@click="darkMode = !darkMode"
|
|
||||||
/>
|
|
||||||
<!-- Admin Settings -->
|
<!-- Admin Settings -->
|
||||||
<IconButton
|
<IconButton
|
||||||
v-if="user?.admin"
|
v-if="user?.admin"
|
||||||
|
@ -62,7 +55,6 @@ import Button from '~/components/atomic/Button.vue';
|
||||||
import IconButton from '~/components/atomic/IconButton.vue';
|
import IconButton from '~/components/atomic/IconButton.vue';
|
||||||
import useAuthentication from '~/compositions/useAuthentication';
|
import useAuthentication from '~/compositions/useAuthentication';
|
||||||
import useConfig from '~/compositions/useConfig';
|
import useConfig from '~/compositions/useConfig';
|
||||||
import { useDarkMode } from '~/compositions/useDarkMode';
|
|
||||||
|
|
||||||
import ActivePipelines from './ActivePipelines.vue';
|
import ActivePipelines from './ActivePipelines.vue';
|
||||||
|
|
||||||
|
@ -70,7 +62,6 @@ const config = useConfig();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const authentication = useAuthentication();
|
const authentication = useAuthentication();
|
||||||
const { user } = authentication;
|
const { user } = authentication;
|
||||||
const { darkMode } = useDarkMode();
|
|
||||||
const apiUrl = `${config.rootPath ?? ''}/swagger/index.html`;
|
const apiUrl = `${config.rootPath ?? ''}/swagger/index.html`;
|
||||||
|
|
||||||
function doLogin() {
|
function doLogin() {
|
||||||
|
|
|
@ -3,6 +3,16 @@
|
||||||
<InputField :label="$t('user.settings.general.language')">
|
<InputField :label="$t('user.settings.general.language')">
|
||||||
<SelectField v-model="selectedLocale" :options="localeOptions" />
|
<SelectField v-model="selectedLocale" :options="localeOptions" />
|
||||||
</InputField>
|
</InputField>
|
||||||
|
<InputField :label="$t('user.settings.general.theme.theme')">
|
||||||
|
<SelectField
|
||||||
|
v-model="storeTheme"
|
||||||
|
:options="[
|
||||||
|
{ value: 'auto', text: $t('user.settings.general.theme.auto') },
|
||||||
|
{ value: 'light', text: $t('user.settings.general.theme.light') },
|
||||||
|
{ value: 'dark', text: $t('user.settings.general.theme.dark') },
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</InputField>
|
||||||
</Settings>
|
</Settings>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -17,8 +27,10 @@ import { useI18n } from 'vue-i18n';
|
||||||
import SelectField from '~/components/form/SelectField.vue';
|
import SelectField from '~/components/form/SelectField.vue';
|
||||||
import Settings from '~/components/layout/Settings.vue';
|
import Settings from '~/components/layout/Settings.vue';
|
||||||
import { setI18nLanguage } from '~/compositions/useI18n';
|
import { setI18nLanguage } from '~/compositions/useI18n';
|
||||||
|
import { useTheme } from '~/compositions/useTheme';
|
||||||
|
|
||||||
const { locale } = useI18n();
|
const { locale } = useI18n();
|
||||||
|
const { storeTheme } = useTheme();
|
||||||
|
|
||||||
const localeOptions = computed(() =>
|
const localeOptions = computed(() =>
|
||||||
SUPPORTED_LOCALES.map((supportedLocale) => ({
|
SUPPORTED_LOCALES.map((supportedLocale) => ({
|
||||||
|
|
|
@ -1,47 +0,0 @@
|
||||||
import { computed, ref, watch } from 'vue';
|
|
||||||
|
|
||||||
const LS_DARK_MODE = 'woodpecker:dark-mode';
|
|
||||||
const isDarkModeActive = ref(false);
|
|
||||||
|
|
||||||
watch(isDarkModeActive, (isActive) => {
|
|
||||||
if (isActive) {
|
|
||||||
document.documentElement.classList.remove('light');
|
|
||||||
document.documentElement.classList.add('dark');
|
|
||||||
document.documentElement.setAttribute('data-theme', 'dark');
|
|
||||||
document.querySelector('meta[name=theme-color]')?.setAttribute('content', '#2A2E3A'); // internal-wp-secondary-600 (see windi.config.ts)
|
|
||||||
} else {
|
|
||||||
document.documentElement.classList.remove('dark');
|
|
||||||
document.documentElement.classList.add('light');
|
|
||||||
document.documentElement.setAttribute('data-theme', 'light');
|
|
||||||
document.querySelector('meta[name=theme-color]')?.setAttribute('content', '#369943'); // internal-wp-primary-400
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function setDarkMode(isActive: boolean) {
|
|
||||||
isDarkModeActive.value = isActive;
|
|
||||||
localStorage.setItem(LS_DARK_MODE, isActive ? 'dark' : 'light');
|
|
||||||
}
|
|
||||||
|
|
||||||
function load() {
|
|
||||||
const isActive = localStorage.getItem(LS_DARK_MODE) as 'dark' | 'light' | null;
|
|
||||||
if (isActive === null) {
|
|
||||||
setDarkMode(window.matchMedia('(prefers-color-scheme: dark)').matches);
|
|
||||||
} else {
|
|
||||||
setDarkMode(isActive === 'dark');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
load();
|
|
||||||
|
|
||||||
export function useDarkMode() {
|
|
||||||
return {
|
|
||||||
darkMode: computed({
|
|
||||||
get() {
|
|
||||||
return isDarkModeActive.value;
|
|
||||||
},
|
|
||||||
set(isActive: boolean) {
|
|
||||||
setDarkMode(isActive);
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,10 +1,11 @@
|
||||||
import { computed, ref, watch } from 'vue';
|
import { computed, ref, watch } from 'vue';
|
||||||
|
|
||||||
import useConfig from '~/compositions/useConfig';
|
import useConfig from '~/compositions/useConfig';
|
||||||
import { useDarkMode } from '~/compositions/useDarkMode';
|
import { useTheme } from '~/compositions/useTheme';
|
||||||
import { PipelineStatus } from '~/lib/api/types';
|
import { PipelineStatus } from '~/lib/api/types';
|
||||||
|
|
||||||
const darkMode = computed(() => (useDarkMode().darkMode.value ? 'dark' : 'light'));
|
const { theme } = useTheme();
|
||||||
|
const darkMode = computed(() => theme.value);
|
||||||
|
|
||||||
type Status = 'default' | 'success' | 'pending' | 'error';
|
type Status = 'default' | 'success' | 'pending' | 'error';
|
||||||
const faviconStatus = ref<Status>('default');
|
const faviconStatus = ref<Status>('default');
|
||||||
|
|
31
web/src/compositions/useTheme.ts
Normal file
31
web/src/compositions/useTheme.ts
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import { useColorMode } from '@vueuse/core';
|
||||||
|
import { watch } from 'vue';
|
||||||
|
|
||||||
|
const { store: storeTheme, state: resolvedTheme } = useColorMode({
|
||||||
|
storageKey: 'woodpecker:theme',
|
||||||
|
});
|
||||||
|
|
||||||
|
function updateTheme() {
|
||||||
|
if (resolvedTheme.value === 'dark') {
|
||||||
|
document.documentElement.classList.remove('light');
|
||||||
|
document.documentElement.classList.add('dark');
|
||||||
|
document.documentElement.setAttribute('data-theme', 'dark');
|
||||||
|
document.querySelector('meta[name=theme-color]')?.setAttribute('content', '#2A2E3A'); // internal-wp-secondary-600 (see windi.config.ts)
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.remove('dark');
|
||||||
|
document.documentElement.classList.add('light');
|
||||||
|
document.documentElement.setAttribute('data-theme', 'light');
|
||||||
|
document.querySelector('meta[name=theme-color]')?.setAttribute('content', '#369943'); // internal-wp-primary-400
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(storeTheme, updateTheme);
|
||||||
|
|
||||||
|
updateTheme();
|
||||||
|
|
||||||
|
export function useTheme() {
|
||||||
|
return {
|
||||||
|
theme: resolvedTheme,
|
||||||
|
storeTheme,
|
||||||
|
};
|
||||||
|
}
|
|
@ -4,6 +4,7 @@
|
||||||
v-for="pipelineConfig in pipelineConfigs || []"
|
v-for="pipelineConfig in pipelineConfigs || []"
|
||||||
:key="pipelineConfig.hash"
|
:key="pipelineConfig.hash"
|
||||||
collapsable
|
collapsable
|
||||||
|
collapsed-by-default
|
||||||
:title="pipelineConfig.name"
|
:title="pipelineConfig.name"
|
||||||
>
|
>
|
||||||
<SyntaxHighlight class="font-mono whitespace-pre overflow-auto" language="yaml" :code="pipelineConfig.data" />
|
<SyntaxHighlight class="font-mono whitespace-pre overflow-auto" language="yaml" :code="pipelineConfig.data" />
|
||||||
|
|
Loading…
Reference in a new issue