feat(ui): v1 desktop responsive — top app bar + content max-widths
CI / test (push) Has been cancelled
CI / test (push) Has been cancelled
The existing PWA layout was mobile-first only: BottomNav hides at ≥960px with no replacement, leaving desktop users with zero navigation and views that stretch to viewport width. Fixes both: - New TopNav.vue mirrors BottomNav (Home / Library / Settings) but renders as a top horizontal app bar at ≥960px only. Includes the wordmark + mark. - App.vue includes <TopNav v-if="!route.meta.hideNav" /> alongside BottomNav so upload-flow hideNav: true still hides both. - HomeView, LibraryView, SettingsView get desktop max-widths (820 / 1100 / 720 respectively) so content centers instead of stretching to 1440+. Same cream/terracotta theme tokens, no aesthetic change — just gives v1 proper desktop chrome. Prep for v2 opt-in landing next. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
|
<TopNav v-if="!route.meta.hideNav" />
|
||||||
<RouterView />
|
<RouterView />
|
||||||
<BottomNav v-if="!route.meta.hideNav" />
|
<BottomNav v-if="!route.meta.hideNav" />
|
||||||
<BaseToast />
|
<BaseToast />
|
||||||
@@ -7,6 +8,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted } from 'vue'
|
import { onMounted } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
|
import TopNav from '@/components/TopNav.vue'
|
||||||
import BottomNav from '@/components/BottomNav.vue'
|
import BottomNav from '@/components/BottomNav.vue'
|
||||||
import BaseToast from '@/components/BaseToast.vue'
|
import BaseToast from '@/components/BaseToast.vue'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
|||||||
@@ -0,0 +1,164 @@
|
|||||||
|
<template>
|
||||||
|
<header class="top-nav" aria-label="Main navigation">
|
||||||
|
<RouterLink to="/" class="top-nav__brand">
|
||||||
|
<span class="top-nav__mark"><img :src="markUrl" alt=""></span>
|
||||||
|
<span class="top-nav__wordmark">WeVisto</span>
|
||||||
|
</RouterLink>
|
||||||
|
<nav class="top-nav__tabs">
|
||||||
|
<RouterLink
|
||||||
|
v-for="tab in tabs"
|
||||||
|
:key="tab.name"
|
||||||
|
:to="tab.to"
|
||||||
|
:class="['top-nav__tab', { 'top-nav__tab--active': tab.isActive(route) }]"
|
||||||
|
:aria-current="tab.isActive(route) ? 'page' : undefined"
|
||||||
|
>
|
||||||
|
<span class="top-nav__icon" aria-hidden="true" v-html="tab.icon" />
|
||||||
|
<span class="top-nav__label">{{ tab.label }}</span>
|
||||||
|
<span
|
||||||
|
v-if="tab.name === 'library' && imagesStore.pendingCount > 0"
|
||||||
|
class="top-nav__badge"
|
||||||
|
>{{ imagesStore.pendingCount > 9 ? '9+' : imagesStore.pendingCount }}</span>
|
||||||
|
</RouterLink>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useRoute, type RouteLocationNormalizedLoaded } from 'vue-router'
|
||||||
|
import { useImagesStore } from '@/stores/images'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const imagesStore = useImagesStore()
|
||||||
|
const markUrl = '/build/icons/apple-touch-icon.png'
|
||||||
|
|
||||||
|
interface Tab {
|
||||||
|
name: string
|
||||||
|
label: string
|
||||||
|
to: string
|
||||||
|
icon: string
|
||||||
|
isActive: (r: RouteLocationNormalizedLoaded) => boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const tabs: Tab[] = [
|
||||||
|
{
|
||||||
|
name: 'home', label: 'Home', to: '/',
|
||||||
|
icon: '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9,22 9,12 15,12 15,22"/></svg>',
|
||||||
|
isActive: r => r.path === '/',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'library', label: 'Library', to: '/library',
|
||||||
|
icon: '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg>',
|
||||||
|
isActive: r => r.path.startsWith('/library'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'settings', label: 'Settings', to: '/settings',
|
||||||
|
icon: '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>',
|
||||||
|
isActive: r => r.path.startsWith('/settings'),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.top-nav {
|
||||||
|
display: none;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 30;
|
||||||
|
background: var(--color-surface);
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
padding-top: env(safe-area-inset-top);
|
||||||
|
|
||||||
|
@media (min-width: 960px) {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding-left: var(--space-6);
|
||||||
|
padding-right: var(--space-6);
|
||||||
|
height: 60px;
|
||||||
|
gap: var(--space-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__brand {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-3);
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__mark {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: var(--color-surface-2);
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
&__mark img { width: 100%; height: 100%; display: block; }
|
||||||
|
|
||||||
|
&__wordmark {
|
||||||
|
font-size: var(--text-lg);
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--space-1);
|
||||||
|
flex: 1;
|
||||||
|
justify-content: flex-start;
|
||||||
|
margin-left: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__tab {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-2);
|
||||||
|
padding: var(--space-2) var(--space-4);
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: var(--text-base);
|
||||||
|
font-weight: 700;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
position: relative;
|
||||||
|
transition: color var(--duration-fast), background var(--duration-fast);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--color-surface-2);
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
&--active {
|
||||||
|
color: var(--color-primary);
|
||||||
|
background: var(--color-surface-2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__label { line-height: 1; }
|
||||||
|
|
||||||
|
&__badge {
|
||||||
|
position: absolute;
|
||||||
|
top: 6px;
|
||||||
|
right: 4px;
|
||||||
|
min-width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
padding: 0 4px;
|
||||||
|
background: var(--color-primary);
|
||||||
|
color: var(--color-primary-fg);
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -774,6 +774,14 @@ async function saveSettings() {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--space-3);
|
gap: var(--space-3);
|
||||||
|
|
||||||
|
// Desktop: cap the width and center; the top nav handles routing here.
|
||||||
|
@media (min-width: 960px) {
|
||||||
|
max-width: 820px;
|
||||||
|
margin: 0 auto;
|
||||||
|
width: 100%;
|
||||||
|
padding-bottom: var(--space-8);
|
||||||
|
}
|
||||||
|
|
||||||
&__loading {
|
&__loading {
|
||||||
color: var(--color-text-muted);
|
color: var(--color-text-muted);
|
||||||
font-size: var(--text-sm);
|
font-size: var(--text-sm);
|
||||||
|
|||||||
@@ -492,12 +492,31 @@ async function doDelete() {
|
|||||||
.library {
|
.library {
|
||||||
padding-bottom: calc(var(--bottom-nav-height) + var(--space-4));
|
padding-bottom: calc(var(--bottom-nav-height) + var(--space-4));
|
||||||
|
|
||||||
|
// Desktop: wider container for grid, but capped; bottom nav is hidden so
|
||||||
|
// the calc value above is harmless padding.
|
||||||
|
@media (min-width: 960px) {
|
||||||
|
max-width: 1100px;
|
||||||
|
margin: 0 auto;
|
||||||
|
width: 100%;
|
||||||
|
padding-bottom: var(--space-8);
|
||||||
|
}
|
||||||
|
|
||||||
&__header {
|
&__header {
|
||||||
padding: var(--space-4) var(--space-4) var(--space-3);
|
padding: var(--space-4) var(--space-4) var(--space-3);
|
||||||
|
|
||||||
|
@media (min-width: 960px) {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-4);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__add-btn {
|
&__add-btn {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
|
@media (min-width: 960px) {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__tabs {
|
&__tabs {
|
||||||
|
|||||||
@@ -259,6 +259,13 @@ async function submitPasswordChange() {
|
|||||||
max-width: 480px;
|
max-width: 480px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
|
||||||
|
// Desktop: roomier; bottom-nav hidden, so reduce the bottom padding.
|
||||||
|
@media (min-width: 960px) {
|
||||||
|
max-width: 720px;
|
||||||
|
padding-top: var(--space-8);
|
||||||
|
padding-bottom: var(--space-8);
|
||||||
|
}
|
||||||
|
|
||||||
&__title {
|
&__title {
|
||||||
font-size: var(--text-xl);
|
font-size: var(--text-xl);
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -16,9 +16,9 @@
|
|||||||
<meta name="mobile-web-app-capable" content="yes" />
|
<meta name="mobile-web-app-capable" content="yes" />
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
||||||
<meta name="apple-mobile-web-app-title" content="WeVisto" />
|
<meta name="apple-mobile-web-app-title" content="WeVisto" />
|
||||||
<script type="module" crossorigin src="/build/assets/index-CH4aAMxd.js"></script>
|
<script type="module" crossorigin src="/build/assets/index-B3RcyMgN.js"></script>
|
||||||
<link rel="modulepreload" crossorigin href="/build/assets/_plugin-vue_export-helper-BNDVmFr7.js">
|
<link rel="modulepreload" crossorigin href="/build/assets/_plugin-vue_export-helper-BNDVmFr7.js">
|
||||||
<link rel="stylesheet" crossorigin href="/build/assets/index-BlLBHR1q.css">
|
<link rel="stylesheet" crossorigin href="/build/assets/index-CraJX9-T.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|||||||
Reference in New Issue
Block a user