Skip to content

Commit

Permalink
feat: introduce new app navbar (#505)
Browse files Browse the repository at this point in the history
  • Loading branch information
HugoRCD authored Feb 27, 2025
1 parent 0f152af commit 9f14612
Show file tree
Hide file tree
Showing 26 changed files with 1,280 additions and 318 deletions.
14 changes: 14 additions & 0 deletions apps/base/assets/css/base.css
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,17 @@ html, body, #__nuxt, #__layout {
opacity: 0.1;
pointer-events: none;
}

.highlight-wrapper {
@apply size-fit p-px transition-all duration-200 ease-in-out;
}

.highlight-gradient {
@apply bg-gradient-to-br from-(--ui-bg-inverted)/60 from-[3%] via-(--ui-bg-elevated) via-40% to-(--ui-bg);
}

.navbar {
.highlight-wrapper {
@apply active:translate-y-[1px] hover:opacity-90 data-[active=true]:bg-gradient-to-br from-(--ui-bg-inverted) from-[3%] via-(--ui-bg-elevated) via-30% to-(--ui-bg);
}
}
24 changes: 24 additions & 0 deletions apps/base/components/BgHighlight.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<script setup lang="ts">
const roundedType = {
none: 'rounded-none',
sm: 'rounded-sm',
md: 'rounded-md',
lg: 'rounded-lg',
xl: 'rounded-xl',
full: 'rounded-full'
}
const props = withDefaults(defineProps<{
rounded?: 'none' | 'sm' | 'md' | 'lg' | 'xl' | 'full'
}>(), {
rounded: 'md'
})
</script>

<template>
<div class="highlight-wrapper highlight-gradient" :class="roundedType[props.rounded]">
<div class="bg-(--ui-bg-elevated)" :class="roundedType[props.rounded]">
<slot />
</div>
</div>
</template>
35 changes: 25 additions & 10 deletions apps/base/components/CustomButton.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,37 @@
type ButtonProps = {
label?: string
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'
rounded?: 'none' | 'sm' | 'md' | 'lg' | 'xl'
to?: string
loading?: boolean
loadingAuto?: boolean
onClick?: ((event: MouseEvent) => void | Promise<void>) | Array<((event: MouseEvent) => void | Promise<void>)>
}
const props = defineProps<ButtonProps>()
const roundedType = {
none: 'rounded-none',
sm: 'rounded-sm',
md: 'rounded-md',
lg: 'rounded-lg',
xl: 'rounded-xl'
}
const props = withDefaults(defineProps<ButtonProps>(), {
rounded: 'md',
})
</script>

<template>
<UButton
v-bind="props"
class="h-fit active:translate-y-[1px] w-fit rounded-lg bg-gradient-to-br dark:from-zinc-200 dark:from-[3%] dark:via-zinc-600 dark:via-30% dark:to-zinc-900 from-zinc-200 from-[3%] via-zinc-300 via-30% to-zinc-500 p-[1px] transition-all duration-200 hover:opacity-90"
<BgHighlight
:rounded="props.rounded"
class="active:translate-y-[1px] hover:opacity-90 shadow-lg dark:shadow-none"
>
<div class="px-2 flex truncate items-center gap-2 py-1 rounded-lg bg-gradient-to-br dark:from-zinc-700 dark:to-zinc-800 from-zinc-100 to-zinc-200 dark:text-white text-zinc-800">
<UButton
v-bind="props"
class="text-(--ui-text-highlighted) bg-transparent hover:bg-transparent disabled:bg-transparent"
:class="roundedType[props.rounded]"
>
<slot v-if="!!$slots.default" />
<span v-else>
{{ label }}
</span>
</div>
</UButton>
</UButton>
</BgHighlight>
</template>
12 changes: 11 additions & 1 deletion apps/base/components/Separator.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
<script setup lang="ts">
const { orientation = 'horizontal' } = defineProps<{
orientation?: 'horizontal' | 'vertical'
}>()
</script>

<template>
<div>
<div v-if="orientation === 'horizontal'">
<USeparator :ui="{ border: 'border-[1px] border-(--ui-bg)' }" />
<USeparator :ui="{ border: 'border-[1px] border-(--ui-bg-elevated)' }" />
</div>
<div v-else class="flex h-full">
<USeparator orientation="vertical" class="h-auto" :ui="{ border: 'border-[4px] border-(--ui-bg)' }" />
<USeparator orientation="vertical" class="h-auto" :ui="{ border: 'border-[4px] border-(--ui-bg-elevated)' }" />
</div>
</template>
1 change: 1 addition & 0 deletions apps/base/nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export default defineNuxtConfig({
'@nuxt/ui',
'nuxt-build-cache',
'nuxt-visitors',
'motion-v/nuxt'
],

nitro: {
Expand Down
3 changes: 2 additions & 1 deletion apps/base/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"iron-webcrypto": "1.2.1",
"nuxt": "3.15.4",
"nuxt-visitors": "1.1.2",
"typescript": "5.7.3"
"typescript": "5.7.3",
"motion-v": "^0.11.0-beta.4"
}
}
2 changes: 1 addition & 1 deletion apps/lp/app/components/layout/Navbar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ const navigationUi = computed(() => ({
<template #right>
<div class="flex items-center gap-2">
<div>
<CustomButton to="https://app.shelve.cloud/login">
<CustomButton to="https://app.shelve.cloud/login" size="xs">
Open App
<UKbd value="S" />
</CustomButton>
Expand Down
3 changes: 2 additions & 1 deletion apps/shelve/app/components/layout/NavItem.vue
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<script setup lang="ts">
import type { Navigation } from '@types'
import { capitalize } from 'vue'
type NavItemProps = {
Expand All @@ -10,7 +11,7 @@ const { navItem, active = false } = defineProps<NavItemProps>()
</script>

<template>
<div class="nav-item select-none" :class="{ active }" :data-active="active" @click="$router.push(navItem.path)">
<div class="nav-item select-none" :class="{ active }" :data-active="active" @click="$router.push(navItem.to)">
<UIcon :name="navItem.icon" class="font-medium" />
<span class="text-sm">
{{ capitalize(navItem.name) }}
Expand Down
180 changes: 180 additions & 0 deletions apps/shelve/app/components/layout/Navbar.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
<script setup lang="ts">
import { Role } from '@types'
import { Motion, LayoutGroup } from 'motion-v'
const route = useRoute()
const teamSlug = computed(() => route.params.teamSlug as string)
const { user } = useUserSession()
const defaultTeamSlug = useCookie<string>('defaultTeamSlug', {
watch: true,
})
const allNavigations = computed(() => {
const team = getNavigation('team', teamSlug.value || defaultTeamSlug.value)
const userNav = getNavigation('user')
const admin = user.value?.role === Role.ADMIN ? getNavigation('admin') : []
return [...team, ...userNav, ...admin]
})
const isSearchActive = ref(false)
const searchQuery = ref('')
const selectedTeamIndex = ref(0)
const loading = useNavbarLoading()
const toggleSearch = () => {
isSearchActive.value = !isSearchActive.value
selectedTeamIndex.value = 0
if (!isSearchActive.value) {
searchQuery.value = ''
} else {
setTimeout(() => {
document.getElementById('search-input')?.focus()
}, 100)
}
}
defineShortcuts({
meta_f: {
usingInput: true,
handler: () => toggleSearch()
},
meta_k: {
usingInput: true,
handler: () => toggleSearch()
},
escape: {
usingInput: true,
handler: () => isSearchActive.value && toggleSearch()
},
arrowdown: {
usingInput: true,
handler: () => {
if (isSearchActive.value) {
selectedTeamIndex.value++
}
}
},
arrowup: {
usingInput: true,
handler: () => {
if (selectedTeamIndex.value > 0) {
selectedTeamIndex.value--
} else {
selectedTeamIndex.value = -1
}
}
},
enter: {
usingInput: true,
handler: () => {}
}
})
</script>

<template>
<div class="navbar-wrapper flex flex-col sm:flex-row sm:items-center gap-4">
<LayoutGroup>
<Motion :layout="true" class="outline-none">
<BgHighlight rounded="full" class="hover:scale-105 cursor-pointer" @click="toggleSearch">
<div class="navbar">
<div class="nav-item p-0.5!">
<UIcon :name="isSearchActive ? 'lucide:x' : 'lucide:search'" class="text-lg" />
</div>
</div>
</BgHighlight>
</Motion>

<Motion
:layout="true"
:initial="{ borderRadius: '9999px' }"
:transition="{ type: 'spring', stiffness: 300, damping: 30 }"
>
<BgHighlight rounded="full">
<Motion :layout="true" class="navbar">
<Motion
v-if="isSearchActive"
:layout="true"
class="search-container"
:initial="{ opacity: 0 }"
:animate="{ opacity: 1 }"
:exit="{ opacity: 0 }"
:transition="{ duration: 0.2 }"
>
<UIcon :name="loading ? 'lucide:loader' : 'lucide:search'" class="icon mr-2" :class="loading ? 'animate-spin' : ''" />
<input
id="search-input"
v-model="searchQuery"
type="text"
placeholder="Search..."
class="bg-transparent border-none outline-none size-full text-(--ui-text-highlighted) placeholder:text-(--ui-text-muted)"
>
</Motion>

<Motion v-else :layout="true" class="flex items-center gap-2">
<Motion
v-for="nav in allNavigations"
:key="nav.to"
:layout="true"
:initial="{ scale: 0.9, opacity: 0 }"
:animate="{ scale: 1, opacity: 1 }"
:transition="{ type: 'spring', stiffness: 500, damping: 30 }"
:class="nav.to.includes('/admin') ? 'hidden sm:flex' : ''"
class="flex-shrink-0"
>
<ULink v-bind="nav">
<UTooltip :text="nav.name" :content="{ side: 'top' }">
<div class="highlight-wrapper rounded-full" :data-active="nav.to === route.path">
<div class="nav-item" :data-active="nav.to === route.path">
<UIcon :name="nav.icon" class="icon" />
</div>
</div>
</UTooltip>
</ULink>
</Motion>
</Motion>
</Motion>
</BgHighlight>
</Motion>
</LayoutGroup>

<TeamManager v-model="isSearchActive" v-model:search="searchQuery" v-model:selected-index="selectedTeamIndex" />
</div>
</template>

<style scoped>
@import "tailwindcss";
.navbar-wrapper {
@apply absolute z-[99] bottom-4 sm:bottom-8 left-1/2 -translate-x-1/2 will-change-auto;
}
.navbar {
@apply backdrop-blur-lg shadow-2xl flex items-center gap-1 sm:gap-2 rounded-full p-2;
width: auto;
}
.search-container {
@apply flex items-center size-full p-1;
width: 320px;
}
.nav-item {
@apply rounded-full p-2 flex items-center justify-center;
@apply data-[active=true]:bg-(--ui-bg-accented) data-[active=true]:shadow-xl bg-transparent;
@apply data-[active=false]:hover:inset-shadow-[2px_2px_2px_rgba(0,0,0,0.2)];
}
.dark {
.nav-item {
@apply data-[active=false]:hover:bg-(--ui-bg-muted) hover:shadow-md;
@apply data-[active=false]:hover:inset-shadow-[2px_2px_5px_rgba(0,0,0,0.4),-2px_-2px_2px_rgba(255,255,255,0.08)];
}
}
.icon {
@apply sm:text-xl text-(--ui-text-highlighted);
}
</style>
Loading

0 comments on commit 9f14612

Please sign in to comment.