wip bsky client for the web & android
bbell.vt3e.cat
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>