wip bsky client for the web & android bbell.vt3e.cat
at main 158 lines 3.3 kB view raw
1<script setup lang="ts"> 2import { nextTick, ref, onMounted, useSlots } from 'vue' 3import { useScrollHide } from '@/composables/useScrollHide' 4 5import AppBar from './AppBar.vue' 6import NavigationBar from './NavigationBar.vue' 7 8const props = defineProps<{ 9 title: string 10 noPadding?: boolean 11}>() 12 13const slots = useSlots() 14const key = 15 Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15) 16 17const pageContent = ref<HTMLElement | null>(null) 18const appBar = ref<InstanceType<typeof AppBar> | null>(null) 19const navBar = ref<InstanceType<typeof NavigationBar> | null>(null) 20 21const announcePageChange = () => { 22 const announcement = document.createElement('div') 23 announcement.setAttribute('aria-live', 'polite') 24 announcement.setAttribute('aria-atomic', 'true') 25 announcement.className = 'sr-only' 26 announcement.textContent = `Navigated to ${props.title}` 27 document.body.appendChild(announcement) 28 29 setTimeout(() => { 30 document.body.removeChild(announcement) 31 }, 1000) 32} 33 34onMounted(async () => { 35 await nextTick() 36 if (!pageContent.value) return 37 38 const scrollHide = useScrollHide({ 39 scrollContainer: pageContent.value, 40 appBarEl: appBar.value?.$el, 41 navBarEl: navBar.value?.$el, 42 }) 43 44 scrollHide.measureElements() 45 scrollHide.attachScrollListener() 46 47 const skipToContent = document.querySelector('#skip-to-content') 48 if (document.activeElement === skipToContent) { 49 pageContent.value.focus() 50 } else { 51 pageContent.value.setAttribute('tabindex', '-1') 52 pageContent.value.focus() 53 } 54 55 announcePageChange() 56}) 57 58const scrollToTop = (smooth = true) => { 59 pageContent.value?.scrollTo({ 60 top: 0, 61 behavior: smooth ? 'smooth' : 'instant', 62 }) 63} 64 65defineExpose({ 66 scrollToTop, 67 scrollContainer: pageContent, 68}) 69</script> 70 71<template> 72 <div class="page-layout" :id="key" :class="{ 'no-padding': noPadding }"> 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> 81 <main class="page-content" ref="pageContent"> 82 <div class="content-container"> 83 <slot /> 84 </div> 85 </main> 86 </div> 87</template> 88 89<style scoped lang="scss"> 90@use '@/assets/variables.scss' as vars; 91 92.page-layout { 93 display: flex; 94 flex-direction: column; 95 max-height: 100%; 96 height: 100%; 97 overflow: hidden; 98 background-color: hsl(var(--base)); 99 position: relative; 100 margin: 0 auto; 101 border: 1px solid transparent; 102} 103 104.page-content { 105 flex: 1; 106 -webkit-overflow-scrolling: touch; 107 height: 100%; 108 overflow-y: scroll; 109 padding-top: calc(var(--inset-top, 0) + 4.5rem); 110 111 display: flex; 112 flex-direction: column; 113 align-items: center; 114 115 .content-container { 116 width: 100%; 117 max-width: 800px; 118 padding: 0 1rem; 119 120 h1, 121 h2, 122 h3, 123 h4, 124 h5, 125 h6 { 126 color: hsl(var(--text)); 127 margin-bottom: 0; 128 } 129 h1 { 130 font-size: 2rem; 131 font-weight: bolder; 132 } 133 h2 { 134 font-size: 1.5rem; 135 font-weight: bolder; 136 } 137 } 138} 139 140@media (min-width: 640px) { 141 .page-layout { 142 border-radius: 1rem; 143 height: calc(100vh - 1rem); 144 margin: 0.5rem; 145 outline: 3px solid vars.$border-colour; 146 outline-offset: -1px; 147 } 148} 149 150.no-padding { 151 .page-content { 152 padding-top: calc(var(--inset-top, 0) + 3.5rem); 153 } 154 .content-container { 155 padding: 0; 156 } 157} 158</style>