Skip to content

Commit 612e94d

Browse files
committed
feat(web): adds floating & reversed navbar
1 parent fccd6ef commit 612e94d

18 files changed

+406
-58
lines changed

src/components/AppComponent.vue

+86-14
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,26 @@ import CheckinComponent from '~/components/views/checkin/CheckinComponent.vue';
1111
import PanelContent from '~/components/views/panel/PanelContent.vue';
1212
import { NavbarService } from '~/services/navbar.service';
1313
import { useAppStateStoreRefs } from '~/stores/app-state.store';
14+
import { useWatchingStoreRefs } from '~/stores/data/watching.store';
1415
import { useAuthSettingsStoreRefs } from '~/stores/settings/auth.store';
1516
1617
const { isAuthenticated } = useAuthSettingsStoreRefs();
1718
const { currentRoute, push, back } = useRouter();
1819
19-
const { footerOpen, panelOpen, panelDirty } = useAppStateStoreRefs();
20+
const {
21+
appRef,
22+
mainRef,
23+
footerRef,
24+
footerOpen,
25+
panelOpen,
26+
panelDirty,
27+
floating,
28+
reverse,
29+
} = useAppStateStoreRefs();
30+
const { isWatching } = useWatchingStoreRefs();
2031
2132
const base = ref();
2233
23-
const appRef = ref<HTMLDivElement>();
24-
const mainRef = ref<HTMLDivElement>();
25-
const footerRef = ref<HTMLDivElement>();
26-
2734
watch(
2835
currentRoute,
2936
(_next, _prev) => {
@@ -62,11 +69,20 @@ onBeforeUnmount(() => {
6269
</script>
6370

6471
<template>
65-
<div ref="appRef" class="app-container">
72+
<div
73+
ref="appRef"
74+
class="app-container"
75+
:class="{ reverse, floating, watching: isWatching }"
76+
>
6677
<NDialogProvider :to="appRef">
6778
<header :class="{ open: panelOpen }">
6879
<RouterView v-slot="{ Component }" name="navbar">
69-
<NavbarComponent v-if="isAuthenticated" :disabled="panelOpen">
80+
<NavbarComponent
81+
v-if="isAuthenticated"
82+
:disabled="panelOpen"
83+
:reverse="reverse"
84+
:floating="floating"
85+
>
7086
<template v-if="Component" #drawer="{ parentElement }">
7187
<Transition name="scale" mode="out-in">
7288
<KeepAlive>
@@ -99,7 +115,7 @@ onBeforeUnmount(() => {
99115
v-model:show="panelOpen"
100116
:to="mainRef"
101117
width="100%"
102-
class="panel"
118+
class="root-panel-wrapper"
103119
close-on-esc
104120
auto-focus
105121
:on-after-leave="onAfterLeave"
@@ -137,17 +153,26 @@ onBeforeUnmount(() => {
137153
@include transition.scale;
138154
139155
.app-container {
156+
--max-header-width: 800px;
157+
140158
overflow: hidden;
141159
142160
header {
143161
position: absolute;
144162
top: 0;
163+
bottom: auto;
164+
left: 0;
145165
z-index: layers.$layer-ui;
146166
display: flex;
147167
flex-direction: column;
148168
justify-content: center;
149169
width: 100%;
150170
min-height: layout.$header-navbar-height;
171+
transition:
172+
bottom 0.4s var(--n-bezier),
173+
top 0.5s var(--n-bezier),
174+
left 0.5s var(--n-bezier),
175+
width 0.5s var(--n-bezier);
151176
152177
> :first-child {
153178
@include mixin.hover-background;
@@ -167,10 +192,13 @@ onBeforeUnmount(() => {
167192
flex-direction: column;
168193
align-items: center;
169194
justify-content: center;
170-
min-height: layout.$main-content-height;
195+
min-height: layout.$safe-content-height;
171196
margin-top: layout.$safe-navbar-height;
172197
padding-right: calc(#{layout.$safe-area-inset-right} / 1.5);
173198
padding-left: calc(#{layout.$safe-area-inset-left} / 1.5);
199+
transition:
200+
min-height 0.5s var(--n-bezier),
201+
margin-top 0.5s var(--n-bezier);
174202
175203
&.full-height {
176204
min-height: var(--full-height);
@@ -189,11 +217,55 @@ onBeforeUnmount(() => {
189217
width: 100%;
190218
}
191219
192-
.panel {
193-
position: relative;
194-
max-height: calc(100% - #{layout.$safe-navbar-height});
195-
overflow: auto;
196-
overscroll-behavior: none;
220+
:deep(.root-panel-wrapper.n-drawer) {
221+
transition: all 0.3s var(--n-bezier);
222+
}
223+
224+
&.watching {
225+
:deep(.root-panel-wrapper.n-drawer) {
226+
padding-bottom: layout.$safe-watching-height;
227+
}
228+
}
229+
230+
&.reverse {
231+
header {
232+
top: auto;
233+
bottom: 0;
234+
flex-direction: column-reverse;
235+
}
236+
237+
main {
238+
min-height: layout.$bottom-content-height;
239+
margin-top: layout.$safe-area-inset-top;
240+
}
241+
242+
footer {
243+
top: 0;
244+
bottom: auto;
245+
}
246+
247+
&.watching {
248+
:deep(.root-panel-wrapper.n-drawer) {
249+
padding-top: layout.$top-safe-watching-height;
250+
}
251+
}
252+
}
253+
254+
&.floating {
255+
header {
256+
top: 1rem;
257+
left: calc(50% - var(--max-header-width) / 2);
258+
width: var(--max-header-width);
259+
}
260+
261+
main {
262+
min-height: layout.$floating-content-height;
263+
margin-top: layout.$safe-area-inset-top;
264+
}
265+
266+
:deep(.root-panel-wrapper.n-drawer) {
267+
margin-top: layout.$floating-navbar-height;
268+
}
197269
}
198270
}
199271
</style>

src/components/common/buttons/FloatingButton.vue

+22-2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { NFlex, NFloatButton, NIcon } from 'naive-ui';
44
import type { Component, PropType, Transition } from 'vue';
55
66
import IconChevronUp from '~/components/icons/IconChevronUp.vue';
7+
import { NavbarService } from '~/services/navbar.service';
8+
import { useAppStateStoreRefs } from '~/stores/app-state.store';
79
import { useWatchingStoreRefs } from '~/stores/data/watching.store';
810
911
defineProps({
@@ -23,6 +25,8 @@ defineProps({
2325
});
2426
2527
const { isWatching } = useWatchingStoreRefs();
28+
const { reverse } = useAppStateStoreRefs();
29+
const { open } = NavbarService;
2630
2731
const emit = defineEmits<{
2832
(e: 'onClick'): void;
@@ -34,7 +38,7 @@ const emit = defineEmits<{
3438
<NFloatButton
3539
v-if="show"
3640
class="button"
37-
:class="{ watching: isWatching }"
41+
:class="{ watching: isWatching, reverse, open }"
3842
width="fit-content"
3943
@click="emit('onClick')"
4044
>
@@ -68,10 +72,26 @@ const emit = defineEmits<{
6872
flex-direction: row;
6973
padding: 0.5rem;
7074
71-
&.watching {
75+
&.watching:not(.reverse) {
7276
bottom: calc(2.5rem + #{layout.$safe-area-inset-bottom / 1.5});
7377
}
7478
79+
&.reverse {
80+
$navbar-offset: calc(
81+
#{layout.$header-navbar-height} + #{layout.$safe-area-inset-bottom / 1.5}
82+
);
83+
84+
bottom: calc(0.5rem + #{$navbar-offset});
85+
transition-delay: 0.5s;
86+
87+
&.open {
88+
$navbar-open-offset: calc(#{layout.$header-drawer-height} + #{$navbar-offset});
89+
90+
bottom: calc(0.5rem + #{$navbar-open-offset});
91+
transition-delay: 0s;
92+
}
93+
}
94+
7595
.text {
7696
display: inline-flex;
7797
width: 0;

src/components/common/list/ListScroll.vue

+33-5
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ import {
1616
type VirtualListProps,
1717
type VirtualListRef,
1818
} from '~/models/list-scroll.model';
19+
import { useAppStateStoreRefs } from '~/stores/app-state.store';
20+
import { useWatchingStoreRefs } from '~/stores/data/watching.store';
21+
import { Watching } from '~/styles/layout.style';
1922
import { watchMedia } from '~/utils/window.utils';
2023
2124
const listRef = ref<VirtualListRef>();
@@ -139,6 +142,9 @@ const {
139142
scrollBoundary,
140143
} = toRefs(props);
141144
145+
const { floating, reverse } = useAppStateStoreRefs();
146+
const { isWatching } = useWatchingStoreRefs();
147+
142148
const scrolled = ref(false);
143149
144150
const isCompact = watchMedia('(max-width: 600px)');
@@ -198,10 +204,19 @@ const topInset = computed(() => {
198204
if (!inset) return 0;
199205
return parseInt(inset, 10);
200206
});
201-
const listPaddingTop = computed(
202-
() => topInset.value + (listOptions?.value?.paddingTop ?? 60),
203-
);
204-
const listPaddingBottom = computed(() => listOptions?.value?.paddingBottom ?? 8);
207+
const listPaddingTop = computed(() => {
208+
const offset = topInset.value;
209+
if (listOptions?.value?.paddingTop) return offset + listOptions.value.paddingTop;
210+
if (floating.value) return offset + 10;
211+
if (reverse.value)
212+
return isWatching.value ? offset + 16 + Watching.Height : offset + 16;
213+
return offset + 60;
214+
});
215+
const listPaddingBottom = computed(() => {
216+
if (listOptions?.value?.paddingBottom) return listOptions.value.paddingBottom;
217+
if (reverse.value) return 8 + 16;
218+
return 8;
219+
});
205220
</script>
206221

207222
<template>
@@ -210,7 +225,10 @@ const listPaddingBottom = computed(() => listOptions?.value?.paddingBottom ?? 8)
210225
v-if="!isEmpty"
211226
ref="listRef"
212227
class="list-scroll"
213-
:style="{ '--overscroll-behavior': overscroll }"
228+
:class="{ floating, reverse }"
229+
:style="{
230+
'--overscroll-behavior': overscroll,
231+
}"
214232
:data-length="items.length"
215233
:data-page-size="pageSize"
216234
:item-size="listItemSize"
@@ -314,6 +332,12 @@ const listPaddingBottom = computed(() => listOptions?.value?.paddingBottom ?? 8)
314332
.list-scroll {
315333
height: var(--full-height);
316334
margin-top: calc(0% - #{layout.$safe-navbar-height});
335+
transition: margin-top 0.5s var(--n-bezier);
336+
337+
&.floating,
338+
&.reverse {
339+
margin-top: calc(0% - #{layout.$safe-area-inset-top});
340+
}
317341
318342
.all-loaded,
319343
.load-more {
@@ -325,5 +349,9 @@ const listPaddingBottom = computed(() => listOptions?.value?.paddingBottom ?? 8)
325349
:deep(.v-vl) {
326350
overscroll-behavior: var(--overscroll-behavior, auto);
327351
}
352+
353+
:deep(.v-vl-items) {
354+
transition: padding 0.5s var(--n-bezier);
355+
}
328356
}
329357
</style>

src/components/common/navbar/NavbarComponent.vue

+31-4
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,19 @@ const props = defineProps({
2323
required: false,
2424
default: false,
2525
},
26+
reverse: {
27+
type: Boolean,
28+
required: false,
29+
default: false,
30+
},
31+
floating: {
32+
type: Boolean,
33+
required: false,
34+
default: false,
35+
},
2636
});
2737
28-
const { disabled } = toRefs(props);
38+
const { disabled, reverse } = toRefs(props);
2939
3040
const i18n = useI18n('route');
3141
const route = useRoute();
@@ -116,10 +126,10 @@ const isDrawerNotScrollable = (
116126
const handleSwipeDirection = (swipe: SwipeDirections) => {
117127
switch (swipe) {
118128
case SwipeDirection.Down:
119-
isHover.value = true;
129+
isHover.value = !reverse.value;
120130
break;
121131
case SwipeDirection.Up:
122-
isHover.value = false;
132+
isHover.value = reverse.value;
123133
break;
124134
case SwipeDirection.Left:
125135
if (nextRoute.value && isDrawerNotScrollable(swipe)) navigate(nextRoute.value);
@@ -157,6 +167,7 @@ const onTouchEnd = (e: TouchEvent) => {
157167
<template>
158168
<nav
159169
ref="navElement"
170+
:class="{ reverse, floating }"
160171
@mouseenter="isHover = true"
161172
@mouseleave="isHover = false"
162173
@focusin="isFocus = true"
@@ -228,9 +239,25 @@ nav {
228239
--navbar-text-color-hover-active: var(--white);
229240
--navbar-text-color-active: var(--white);
230241
242+
display: flex;
243+
flex-direction: column;
231244
padding: env(safe-area-inset-top) 0.25rem 0;
245+
overflow-x: unset;
232246
font-size: 12px;
233247
text-align: center;
248+
transition:
249+
border-radius 0.25s var(--n-bezier),
250+
padding 0.25s var(--n-bezier);
251+
252+
&.reverse {
253+
flex-direction: column-reverse;
254+
padding: 0 0.25rem env(safe-area-inset-bottom);
255+
}
256+
257+
&.floating {
258+
padding: 0 0.25rem;
259+
border-radius: 1rem;
260+
}
234261
235262
.drawer {
236263
@include layout.navbar-transition;
@@ -260,7 +287,7 @@ nav {
260287
:deep(.tab) {
261288
--n-font-weight-strong: normal;
262289
263-
padding: 0 0.25rem;
290+
padding: 0 0.75rem;
264291
}
265292
}
266293
}

0 commit comments

Comments
 (0)