modularize service layer, add mobile nav drawer, and sync docs
This commit is contained in:
parent
7f7a8ce2b0
commit
6b6ed91b92
72 changed files with 8122 additions and 7501 deletions
|
|
@ -1,5 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { RouterView, useRoute } from "vue-router";
|
||||
|
||||
import AppHeader from "@/components/layout/AppHeader.vue";
|
||||
|
|
@ -11,6 +11,28 @@ const auth = useAuthStore();
|
|||
const route = useRoute();
|
||||
const showChrome = computed(() => auth.isAuthenticated);
|
||||
const shouldLockMainScroll = computed(() => route.meta.lockMainScroll === true);
|
||||
const isMobileNavOpen = ref(false);
|
||||
|
||||
function closeMobileNav(): void {
|
||||
isMobileNavOpen.value = false;
|
||||
}
|
||||
|
||||
function toggleMobileNav(): void {
|
||||
isMobileNavOpen.value = !isMobileNavOpen.value;
|
||||
}
|
||||
|
||||
watch(
|
||||
() => route.fullPath,
|
||||
() => {
|
||||
closeMobileNav();
|
||||
},
|
||||
);
|
||||
|
||||
watch(showChrome, (value) => {
|
||||
if (!value) {
|
||||
closeMobileNav();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -19,14 +41,34 @@ const shouldLockMainScroll = computed(() => route.meta.lockMainScroll === true);
|
|||
<RequestErrorPanel />
|
||||
|
||||
<template v-if="showChrome">
|
||||
<AppHeader />
|
||||
<AppHeader
|
||||
:show-menu-button="true"
|
||||
:menu-open="isMobileNavOpen"
|
||||
@toggle-menu="toggleMobileNav"
|
||||
/>
|
||||
<div
|
||||
class="grid h-[calc(100dvh-4.5rem)] min-h-0 grid-cols-1 grid-rows-[auto_minmax(0,1fr)] lg:grid-cols-[17rem_minmax(0,1fr)] lg:grid-rows-1"
|
||||
class="relative h-[calc(100dvh-4.5rem)] min-h-0 lg:grid lg:grid-cols-[17rem_minmax(0,1fr)] lg:grid-rows-1"
|
||||
>
|
||||
<AppNav class="min-h-0 overflow-x-hidden lg:overflow-y-auto" />
|
||||
<button
|
||||
v-if="isMobileNavOpen"
|
||||
type="button"
|
||||
class="absolute inset-0 z-30 bg-black/35 backdrop-blur-[1px] lg:hidden"
|
||||
aria-label="Close navigation menu"
|
||||
@click="closeMobileNav"
|
||||
/>
|
||||
<div
|
||||
id="mobile-primary-nav"
|
||||
class="absolute inset-y-0 left-0 z-40 w-[17rem] max-w-[85vw] min-h-0 bg-surface-nav shadow-xl transition-transform duration-200 ease-out lg:static lg:z-auto lg:w-auto lg:max-w-none lg:bg-transparent lg:shadow-none lg:transition-none"
|
||||
:class="isMobileNavOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'"
|
||||
>
|
||||
<AppNav
|
||||
class="h-full min-h-0 overflow-x-hidden overflow-y-auto lg:h-auto lg:overflow-y-auto"
|
||||
@navigate="closeMobileNav"
|
||||
/>
|
||||
</div>
|
||||
<main
|
||||
id="app-main"
|
||||
class="min-h-0 min-w-0 overflow-x-hidden px-4 py-6 sm:px-6 lg:px-8"
|
||||
class="min-h-0 min-w-0 overflow-x-hidden px-4 py-6 sm:px-6 lg:col-start-2 lg:px-8"
|
||||
:class="shouldLockMainScroll ? 'overflow-y-auto lg:overflow-y-hidden' : 'overflow-y-auto'"
|
||||
>
|
||||
<RouterView />
|
||||
|
|
|
|||
|
|
@ -6,12 +6,29 @@ import AppSelect from "@/components/ui/AppSelect.vue";
|
|||
import { useThemeStore } from "@/stores/theme";
|
||||
import type { ThemePresetId } from "@/theme/presets";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
showMenuButton?: boolean;
|
||||
menuOpen?: boolean;
|
||||
}>(),
|
||||
{
|
||||
showMenuButton: false,
|
||||
menuOpen: false,
|
||||
},
|
||||
);
|
||||
const emit = defineEmits<{
|
||||
(e: "toggle-menu"): void;
|
||||
}>();
|
||||
|
||||
const theme = useThemeStore();
|
||||
|
||||
const isDarkTheme = computed(() => theme.active === "dark");
|
||||
const toggleThemeLabel = computed(() =>
|
||||
isDarkTheme.value ? "Switch to light theme" : "Switch to dark theme",
|
||||
);
|
||||
const menuButtonLabel = computed(() =>
|
||||
props.menuOpen ? "Close navigation menu" : "Open navigation menu",
|
||||
);
|
||||
const selectedPreset = computed<ThemePresetId>({
|
||||
get: () => theme.preset,
|
||||
set: (value) => theme.setPreset(value),
|
||||
|
|
@ -22,12 +39,40 @@ const themePresetLabel = computed(() => theme.presetLabel);
|
|||
function onToggleTheme(): void {
|
||||
theme.setPreference(isDarkTheme.value ? "light" : "dark");
|
||||
}
|
||||
|
||||
function onToggleMenu(): void {
|
||||
emit("toggle-menu");
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header class="sticky top-0 z-30 border-b border-stroke-subtle bg-surface-nav/90 backdrop-blur">
|
||||
<div class="flex min-h-[4.5rem] w-full items-center justify-between gap-4 px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex min-w-0 items-center gap-2">
|
||||
<AppButton
|
||||
v-if="props.showMenuButton"
|
||||
variant="ghost"
|
||||
class="h-10 w-10 rounded-full p-0 lg:hidden"
|
||||
:aria-label="menuButtonLabel"
|
||||
:aria-expanded="props.menuOpen"
|
||||
aria-controls="mobile-primary-nav"
|
||||
:title="menuButtonLabel"
|
||||
@click="onToggleMenu"
|
||||
>
|
||||
<span class="sr-only">{{ menuButtonLabel }}</span>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.8"
|
||||
class="h-5 w-5"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path v-if="props.menuOpen" d="M6 6l12 12M18 6 6 18" />
|
||||
<path v-else d="M4 7h16M4 12h16M4 17h16" />
|
||||
</svg>
|
||||
</AppButton>
|
||||
<RouterLink
|
||||
to="/dashboard"
|
||||
class="rounded-sm font-display text-lg tracking-tight text-ink-primary transition hover:text-ink-secondary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring focus-visible:ring-offset-2 focus-visible:ring-offset-focus-offset"
|
||||
|
|
|
|||
|
|
@ -14,6 +14,9 @@ const auth = useAuthStore();
|
|||
const runStatus = useRunStatusStore();
|
||||
const userSettings = useUserSettingsStore();
|
||||
const router = useRouter();
|
||||
const emit = defineEmits<{
|
||||
(e: "navigate"): void;
|
||||
}>();
|
||||
|
||||
const links = computed(() => {
|
||||
const base = [
|
||||
|
|
@ -63,9 +66,14 @@ const showSafetyRow = computed(
|
|||
);
|
||||
|
||||
async function onLogout(): Promise<void> {
|
||||
emit("navigate");
|
||||
await auth.logout();
|
||||
await router.replace({ name: "login" });
|
||||
}
|
||||
|
||||
function onNavigate(): void {
|
||||
emit("navigate");
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -80,6 +88,7 @@ async function onLogout(): Promise<void> {
|
|||
:to="link.to"
|
||||
class="inline-flex min-h-10 min-w-0 items-center rounded-lg border border-transparent px-3 py-2 text-sm font-medium text-ink-secondary transition hover:border-stroke-interactive hover:bg-surface-card-muted hover:text-ink-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring focus-visible:ring-offset-2 focus-visible:ring-offset-focus-offset"
|
||||
active-class="border-stroke-interactive bg-surface-nav-active font-semibold text-ink-primary shadow-sm hover:border-stroke-interactive hover:bg-surface-nav-active hover:text-ink-primary"
|
||||
@click="onNavigate"
|
||||
>
|
||||
{{ link.label }}
|
||||
</RouterLink>
|
||||
|
|
|
|||
|
|
@ -861,7 +861,7 @@ onMounted(() => {
|
|||
body="Clear or adjust the filter to see tracked scholars."
|
||||
/>
|
||||
|
||||
<div v-else class="space-y-3 xl:flex xl:h-full xl:min-h-0 xl:flex-col xl:space-y-0">
|
||||
<div v-else class="space-y-3 xl:flex xl:h-full xl:min-h-0 xl:flex-col xl:space-y-0 xl:overflow-hidden">
|
||||
<ul class="grid gap-3 lg:hidden">
|
||||
<li
|
||||
v-for="item in visibleScholars"
|
||||
|
|
@ -888,8 +888,8 @@ onMounted(() => {
|
|||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="hidden min-h-0 flex-1 lg:block">
|
||||
<AppTable class="max-h-full overflow-y-auto" label="Tracked scholars table">
|
||||
<div class="hidden min-h-0 flex-1 overflow-hidden lg:block">
|
||||
<AppTable class="h-full overflow-y-scroll overscroll-contain" label="Tracked scholars table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Scholar</th>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue