wip bsky client for the web & android
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: allow passing in custom app bar; replace logo with link to signed in user's profile

willow 2faa31e5 082e5cce

+93 -43
+25 -21
src/components/Navigation/AppBar.vue
··· 1 1 <script setup lang="ts"> 2 2 import { computed, ref, onMounted } from 'vue' 3 3 import { IconArrowBackRounded } from '@iconify-prerendered/vue-material-symbols' 4 - import { useNavigationStore, type StackEntry } from '@/stores/navigation' 4 + import { useNavigationStore, type StackEntry, type TabKey } from '@/stores/navigation' 5 5 6 6 const appBarElement = ref<HTMLElement>() 7 7 const overlayElement = ref<HTMLElement>() ··· 33 33 34 34 const activeTab = computed(() => nav.activeTab) 35 35 const stack = computed(() => { 36 - const tab = contextTab.value ?? activeTab.value 37 - return nav.stacks[tab] 36 + const tab = (contextTab.value ?? activeTab.value) as TabKey 37 + const _stack = nav.stacks[tab] 38 + if (!_stack) return null 39 + return _stack 38 40 }) 39 41 40 42 const canGoBack = computed(() => { ··· 85 87 > 86 88 <div class="topbar-buffer" aria-hidden="true"></div> 87 89 <div class="topbar-content"> 88 - <div class="topbar-left"> 89 - <button 90 - v-if="canGoBack" 91 - @click="goBack" 92 - class="topbar-icon-btn" 93 - aria-label="Go back" 94 - type="button" 95 - > 96 - <IconArrowBackRounded class="icon" aria-hidden="true" /> 97 - </button> 90 + <slot name="content"> 91 + <div class="topbar-left"> 92 + <button 93 + v-if="canGoBack" 94 + @click="goBack" 95 + class="topbar-icon-btn" 96 + aria-label="Go back" 97 + type="button" 98 + > 99 + <IconArrowBackRounded class="icon" aria-hidden="true" /> 100 + </button> 98 101 99 - <h1 class="topbar-title" id="page-title"> 100 - {{ displayTitle }} 101 - </h1> 102 - </div> 103 - <div class="topbar-right" role="toolbar" aria-label="Page actions"> 104 - <slot name="actions" /> 105 - </div> 102 + <h1 class="topbar-title" id="page-title"> 103 + {{ displayTitle }} 104 + </h1> 105 + </div> 106 + <div class="topbar-right" role="toolbar" aria-label="Page actions"> 107 + <slot name="actions" /> 108 + </div> 109 + </slot> 106 110 </div> 107 111 </header> 108 112 </template> ··· 120 124 121 125 .topbar { 122 126 background: hsla(var(--base) / 0.8); 123 - backdrop-filter: blur(12px); 127 + backdrop-filter: blur(0.75rem); 124 128 border-bottom: 1px solid hsla(var(--surface2) / 0.3); 125 129 126 130 position: absolute;
+46 -20
src/components/Navigation/NavigationBar.vue
··· 1 1 <script setup lang="ts"> 2 2 import { ref } from 'vue' 3 + 4 + import { stackRoots } from '@/router/index' 5 + import AppLink from '@/components/Navigation/AppLink.vue' 3 6 import NavigationItem from './NavItem.vue' 4 - import SVG from '@/components/UI/SVG.vue' 5 - import { stackRoots } from '@/router/index' 6 - import BluebellLogo from '@/assets/icons/bluebell.svg?raw' 7 7 8 8 import { useNavigationStore } from '@/stores/navigation' 9 + import { useAuthStore } from '@/stores/auth' 10 + 9 11 const nav = useNavigationStore() 12 + const auth = useAuthStore() 10 13 11 14 const navRef = ref<HTMLElement>() 12 15 defineExpose({ $el: navRef }) ··· 20 23 role="tablist" 21 24 aria-label="Main navigation" 22 25 > 23 - <ul class="menu" role="presentation"> 24 - <li class="desktop-logo" role="presentation"> 25 - <SVG :icon="BluebellLogo"></SVG> 26 + <ul class="menu"> 27 + <li class="profile-item"> 28 + <AppLink 29 + v-if="auth.profile && auth.isAuthenticated" 30 + name="user-profile" 31 + :params="{ id: auth.profile.handle }" 32 + class="profile-link" 33 + > 34 + <img :src="auth.profile.avatar" class="profile-avatar" role="presentation" /> 35 + </AppLink> 36 + <div v-else class="profile-placeholder"></div> 26 37 </li> 27 38 28 39 <NavigationItem ··· 55 66 color, background-color, box-shadow, outline, border-color, border-radius, font-weight, opacity, 56 67 backdrop-filter, filter, width, bottom, left; 57 68 58 - .desktop-logo { 69 + .profile-item { 59 70 display: none; 60 - color: white; 71 + align-items: center; 72 + border: 2px solid transparent; 61 73 62 - :deep(svg) { 63 - background: hsla(var(--accent) / 0.05); 64 - color: hsla(var(--accent) / 1); 65 - border-radius: var(--radius-sm); 74 + .profile-link { 75 + display: block; 76 + border-radius: 50%; 77 + overflow: hidden; 78 + border: 2px solid hsla(var(--surface2) / 0.5); 79 + 66 80 &:hover { 67 81 transform: scale(1.1); 68 - background: hsla(var(--accent) / 0.1); 82 + border-color: hsl(var(--accent)); 69 83 } 84 + 70 85 &:active { 71 86 transform: scale(0.95); 72 87 } 73 88 } 89 + 90 + .profile-avatar { 91 + width: 100%; 92 + height: 100%; 93 + object-fit: cover; 94 + background-color: hsl(var(--surface1)); 95 + } 96 + 97 + .profile-placeholder { 98 + width: 2rem; 99 + height: 2rem; 100 + } 74 101 } 75 102 76 103 .menu { ··· 96 123 backdrop-filter: none; 97 124 order: -1; 98 125 99 - .desktop-logo { 126 + .profile-item { 100 127 display: flex; 101 - justify-content: center; 102 128 padding: 0.5rem; 129 + margin-bottom: 0.5rem; 130 + } 103 131 104 - :deep(svg) { 105 - width: 2.5rem; 106 - height: 2.5rem; 107 - display: block; 108 - } 132 + .profile-link { 133 + width: 2.5rem; 134 + height: 2.5rem; 109 135 } 110 136 111 137 .menu {
+22 -2
src/components/Navigation/PageLayout.vue
··· 1 1 <script setup lang="ts"> 2 - import { nextTick, ref, onMounted } from 'vue' 2 + import { nextTick, ref, onMounted, useSlots } from 'vue' 3 3 import { useScrollHide } from '@/composables/useScrollHide' 4 4 5 5 import AppBar from './AppBar.vue' ··· 10 10 noPadding?: boolean 11 11 }>() 12 12 13 + const slots = useSlots() 13 14 const key = 14 15 Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15) 15 16 ··· 53 54 54 55 announcePageChange() 55 56 }) 57 + 58 + const scrollToTop = (smooth = true) => { 59 + pageContent.value?.scrollTo({ 60 + top: 0, 61 + behavior: smooth ? 'smooth' : 'instant', 62 + }) 63 + } 64 + 65 + defineExpose({ 66 + scrollToTop, 67 + scrollContainer: pageContent, 68 + }) 56 69 </script> 57 70 58 71 <template> 59 72 <div class="page-layout" :id="key" :class="{ 'no-padding': noPadding }"> 60 - <AppBar :title="title" ref="appBar" /> 73 + <AppBar :title="title" ref="appBar"> 74 + <template #content v-if="slots['app-bar']"> 75 + <slot name="app-bar" /> 76 + </template> 77 + <template #actions v-if="slots['actions']"> 78 + <slot name="actions" /> 79 + </template> 80 + </AppBar> 61 81 <main class="page-content" ref="pageContent"> 62 82 <div class="content-container"> 63 83 <slot />