mirror of
https://github.com/woodpecker-ci/woodpecker.git
synced 2025-10-21 21:03:22 +00:00
Navbar Icons Improvements (#1246)
Some improvements to the navbar icons. Changes Implemented: - Increase touch target size for navbar icons. - Make icon colors and hover effect consistent with navbar links - Added title for all navbar icons - New key (user.settings) in locales - Updated Dark and Light Mode values in locales - Minor tweaks in active builds indicator - New component NavbarIcon (because trying to match IconButton size and colors felt hacky at best) Co-authored-by: Divya Jain <dvjn.dev+git@gmail.com>
This commit is contained in:
1
web/components.d.ts
vendored
1
web/components.d.ts
vendored
@@ -65,6 +65,7 @@ declare module '@vue/runtime-core' {
|
|||||||
ListItem: typeof import('./src/components/atomic/ListItem.vue')['default']
|
ListItem: typeof import('./src/components/atomic/ListItem.vue')['default']
|
||||||
ManualPipelinePopup: typeof import('./src/components/layout/popups/ManualPipelinePopup.vue')['default']
|
ManualPipelinePopup: typeof import('./src/components/layout/popups/ManualPipelinePopup.vue')['default']
|
||||||
Navbar: typeof import('./src/components/layout/header/Navbar.vue')['default']
|
Navbar: typeof import('./src/components/layout/header/Navbar.vue')['default']
|
||||||
|
NavbarIcon: typeof import('./src/components/layout/header/NavbarIcon.vue')['default']
|
||||||
NumberField: typeof import('./src/components/form/NumberField.vue')['default']
|
NumberField: typeof import('./src/components/form/NumberField.vue')['default']
|
||||||
OrgSecretsTab: typeof import('./src/components/org/settings/OrgSecretsTab.vue')['default']
|
OrgSecretsTab: typeof import('./src/components/org/settings/OrgSecretsTab.vue')['default']
|
||||||
Panel: typeof import('./src/components/layout/Panel.vue')['default']
|
Panel: typeof import('./src/components/layout/Panel.vue')['default']
|
||||||
|
@@ -10,8 +10,8 @@
|
|||||||
"password": "Password",
|
"password": "Password",
|
||||||
"url": "URL",
|
"url": "URL",
|
||||||
"back": "Back",
|
"back": "Back",
|
||||||
"color_scheme_light": "Switch between dark and light mode (currently light mode)",
|
"color_scheme_light": "Switch to dark mode",
|
||||||
"color_scheme_dark": "Switch between dark and light mode (currently dark mode)",
|
"color_scheme_dark": "Switch to light mode",
|
||||||
"unknown_error": "An unknown error occurred",
|
"unknown_error": "An unknown error occurred",
|
||||||
"not_found": {
|
"not_found": {
|
||||||
"not_found": "Whoa 404, either we broke something or you had a typing mishap :-/",
|
"not_found": "Whoa 404, either we broke something or you had a typing mishap :-/",
|
||||||
@@ -292,6 +292,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"user": {
|
"user": {
|
||||||
|
"settings": "User Settings",
|
||||||
"oauth_error": "Error while authenticating against OAuth provider",
|
"oauth_error": "Error while authenticating against OAuth provider",
|
||||||
"internal_error": "Some internal error occurred",
|
"internal_error": "Some internal error occurred",
|
||||||
"access_denied": "You are not allowed to login",
|
"access_denied": "You are not allowed to login",
|
||||||
|
@@ -8,13 +8,7 @@
|
|||||||
@click="doClick"
|
@click="doClick"
|
||||||
>
|
>
|
||||||
<Icon :name="icon" />
|
<Icon :name="icon" />
|
||||||
<div
|
<div v-if="isLoading" class="absolute left-0 top-0 right-0 bottom-0 flex items-center justify-center">
|
||||||
class="absolute left-0 top-0 right-0 bottom-0 flex items-center justify-center"
|
|
||||||
:class="{
|
|
||||||
'opacity-100': isLoading,
|
|
||||||
'opacity-0': !isLoading,
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<Icon name="loading" class="animate-spin" />
|
<Icon name="loading" class="animate-spin" />
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
@@ -1,18 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<button
|
<NavbarIcon :title="$t('repo.pipeline.tasks')" class="!p-1.5 relative" @click="toggle">
|
||||||
class="flex rounded-full w-8 h-8 bg-opacity-30 hover:bg-opacity-50 bg-white items-center justify-center cursor-pointer text-white select-none"
|
<div v-if="activePipelines.length > 0" class="spinner">
|
||||||
:class="{
|
<div class="spinner-ring ring1" />
|
||||||
spinner: activePipelines.length !== 0,
|
<div class="spinner-ring ring2" />
|
||||||
}"
|
<div class="spinner-ring ring3" />
|
||||||
type="button"
|
<div class="spinner-ring ring4" />
|
||||||
@click="toggle"
|
</div>
|
||||||
>
|
<div
|
||||||
<div class="spinner-ring ring1" />
|
class="flex items-center justify-center h-full w-full font-bold bg-white bg-opacity-15 dark:bg-black dark:bg-opacity-10 rounded-full"
|
||||||
<div class="spinner-ring ring2" />
|
>
|
||||||
<div class="spinner-ring ring3" />
|
{{ activePipelines.length || 0 }}
|
||||||
<div class="spinner-ring ring4" />
|
</div>
|
||||||
{{ activePipelines.length || 0 }}
|
</NavbarIcon>
|
||||||
</button>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
@@ -20,9 +19,13 @@ import { defineComponent, onMounted } from 'vue';
|
|||||||
|
|
||||||
import usePipelineFeed from '~/compositions/usePipelineFeed';
|
import usePipelineFeed from '~/compositions/usePipelineFeed';
|
||||||
|
|
||||||
|
import NavbarIcon from './NavbarIcon.vue';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'ActivePipelines',
|
name: 'ActivePipelines',
|
||||||
|
|
||||||
|
components: { NavbarIcon },
|
||||||
|
|
||||||
setup() {
|
setup() {
|
||||||
const pipelineFeed = usePipelineFeed();
|
const pipelineFeed = usePipelineFeed();
|
||||||
|
|
||||||
@@ -36,10 +39,13 @@ export default defineComponent({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.spinner {
|
||||||
|
@apply absolute top-0 bottom-0 left-0 right-0;
|
||||||
|
}
|
||||||
.spinner .spinner-ring {
|
.spinner .spinner-ring {
|
||||||
animation: spinner 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
|
animation: spinner 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
|
||||||
border-color: #fff transparent transparent transparent;
|
border-color: #fff transparent transparent transparent;
|
||||||
@apply w-8 h-8 border-2 rounded-full m-4 absolute;
|
@apply border-3 rounded-full absolute top-1.5 bottom-1.5 left-1.5 right-1.5;
|
||||||
}
|
}
|
||||||
.spinner .ring1 {
|
.spinner .ring1 {
|
||||||
animation-delay: -0.45s;
|
animation-delay: -0.45s;
|
||||||
|
@@ -9,36 +9,45 @@
|
|||||||
<span class="text-xs">{{ version }}</span>
|
<span class="text-xs">{{ version }}</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
<!-- Repo Link -->
|
<!-- Repo Link -->
|
||||||
<router-link v-if="user" :to="{ name: 'repos' }" class="navbar-link">
|
<router-link v-if="user" :to="{ name: 'repos' }" class="navbar-link navbar-clickable">
|
||||||
<span class="flex md:hidden">{{ $t('repos') }}</span>
|
<span class="flex md:hidden">{{ $t('repos') }}</span>
|
||||||
<span class="hidden md:flex">{{ $t('repositories') }}</span>
|
<span class="hidden md:flex">{{ $t('repositories') }}</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
<!-- Docs Link -->
|
<!-- Docs Link -->
|
||||||
<a :href="docsUrl" target="_blank" class="navbar-link hidden md:flex">{{ $t('docs') }}</a>
|
<a :href="docsUrl" target="_blank" class="navbar-link navbar-clickable hidden md:flex">{{ $t('docs') }}</a>
|
||||||
</div>
|
</div>
|
||||||
<!-- Right Icons Box -->
|
<!-- Right Icons Box -->
|
||||||
<div class="flex ml-auto items-center space-x-3 text-white dark:text-gray-400">
|
<div class="flex ml-auto -m-1.5 items-center space-x-2 text-white dark:text-gray-400">
|
||||||
<!-- Dark Mode Toggle -->
|
<!-- Dark Mode Toggle -->
|
||||||
<IconButton
|
<NavbarIcon
|
||||||
:icon="darkMode ? 'dark' : 'light'"
|
:title="$t(darkMode ? 'color_scheme_dark' : 'color_scheme_light')"
|
||||||
class="!text-white !dark:text-gray-500 navbar-icon"
|
class="navbar-icon navbar-clickable"
|
||||||
:title="darkMode ? $t('color_scheme_dark') : $t('color_scheme_light')"
|
|
||||||
@click="darkMode = !darkMode"
|
@click="darkMode = !darkMode"
|
||||||
/>
|
>
|
||||||
|
<i-ic-baseline-dark-mode v-if="darkMode" />
|
||||||
|
<i-ic-round-light-mode v-else />
|
||||||
|
</NavbarIcon>
|
||||||
<!-- Admin Settings -->
|
<!-- Admin Settings -->
|
||||||
<IconButton
|
<NavbarIcon
|
||||||
v-if="user?.admin"
|
v-if="user?.admin"
|
||||||
icon="settings"
|
class="navbar-icon navbar-clickable"
|
||||||
class="!text-white !dark:text-gray-500 navbar-icon"
|
|
||||||
:title="$t('admin.settings.settings')"
|
:title="$t('admin.settings.settings')"
|
||||||
:to="{ name: 'admin-settings' }"
|
:to="{ name: 'admin-settings' }"
|
||||||
/>
|
>
|
||||||
<!-- Active Builds Indicator -->
|
<i-clarity-settings-solid />
|
||||||
<ActivePipelines v-if="user" />
|
</NavbarIcon>
|
||||||
|
|
||||||
|
<!-- Active Pipelines Indicator -->
|
||||||
|
<ActivePipelines v-if="user" class="navbar-icon navbar-clickable" />
|
||||||
<!-- User Avatar -->
|
<!-- User Avatar -->
|
||||||
<router-link v-if="user" :to="{ name: 'user' }" class="rounded-full overflow-hidden">
|
<NavbarIcon
|
||||||
<img v-if="user && user.avatar_url" class="navbar-icon" :src="`${user.avatar_url}`" />
|
v-if="user"
|
||||||
</router-link>
|
:to="{ name: 'user' }"
|
||||||
|
:title="$t('user.settings')"
|
||||||
|
class="navbar-icon navbar-clickable !p-1.5"
|
||||||
|
>
|
||||||
|
<img v-if="user && user.avatar_url" class="rounded-full" :src="`${user.avatar_url}`" />
|
||||||
|
</NavbarIcon>
|
||||||
<!-- Login Button -->
|
<!-- Login Button -->
|
||||||
<Button v-else :text="$t('login')" @click="doLogin" />
|
<Button v-else :text="$t('login')" @click="doLogin" />
|
||||||
</div>
|
</div>
|
||||||
@@ -50,17 +59,17 @@ import { defineComponent } from 'vue';
|
|||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
|
|
||||||
import Button from '~/components/atomic/Button.vue';
|
import Button from '~/components/atomic/Button.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 { useDarkMode } from '~/compositions/useDarkMode';
|
||||||
|
|
||||||
import ActivePipelines from './ActivePipelines.vue';
|
import ActivePipelines from './ActivePipelines.vue';
|
||||||
|
import NavbarIcon from './NavbarIcon.vue';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'Navbar',
|
name: 'Navbar',
|
||||||
|
|
||||||
components: { Button, ActivePipelines, IconButton },
|
components: { Button, ActivePipelines, NavbarIcon },
|
||||||
|
|
||||||
setup() {
|
setup() {
|
||||||
const config = useConfig();
|
const config = useConfig();
|
||||||
@@ -82,10 +91,10 @@ export default defineComponent({
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.navbar-link {
|
.navbar-link {
|
||||||
@apply hover:bg-black hover:bg-opacity-10 transition-colors duration-100 px-3 py-2 -my-1 rounded-md;
|
@apply px-3 py-2 -my-1 rounded-md;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-icon {
|
.navbar-clickable {
|
||||||
@apply w-8 h-8;
|
@apply hover:bg-black hover:bg-opacity-10 dark:hover:bg-white dark:hover:bg-opacity-5 transition-colors duration-100;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
55
web/src/components/layout/header/NavbarIcon.vue
Normal file
55
web/src/components/layout/header/NavbarIcon.vue
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<template>
|
||||||
|
<button type="button" :title="title" :aria-label="title" class="navbar-icon" @click="doClick">
|
||||||
|
<slot />
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, PropType } from 'vue';
|
||||||
|
import { RouteLocationRaw, useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'NavbarIcon',
|
||||||
|
|
||||||
|
props: {
|
||||||
|
to: {
|
||||||
|
type: [String, Object, null] as PropType<RouteLocationRaw | null>,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
setup(props) {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
async function doClick() {
|
||||||
|
if (!props.to) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof props.to === 'string' && props.to.startsWith('http')) {
|
||||||
|
window.location.href = props.to;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await router.push(props.to);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { doClick };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.navbar-icon {
|
||||||
|
@apply w-11 h-11 rounded-full p-2.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-icon :deep(svg) {
|
||||||
|
@apply w-full h-full;
|
||||||
|
}
|
||||||
|
</style>
|
Reference in New Issue
Block a user