feat: re-design
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useListsStore } from '@/stores/lists'
|
||||
@@ -11,25 +12,49 @@ async function handleLogout() {
|
||||
await authStore.logout()
|
||||
router.push('/login')
|
||||
}
|
||||
|
||||
const stampLabel = computed(() => {
|
||||
if (!authStore.isAuthenticated) return 'offline'
|
||||
if (listsStore.isSyncing) return 'syncing'
|
||||
if (listsStore.pendingCount > 0) return `${listsStore.pendingCount} pending`
|
||||
return 'synced'
|
||||
})
|
||||
|
||||
const stampClass = computed(() => {
|
||||
if (!authStore.isAuthenticated) return ''
|
||||
if (listsStore.pendingCount > 0 || listsStore.isSyncing) return 'stamp-pending'
|
||||
return 'stamp-synced'
|
||||
})
|
||||
|
||||
const stampTitle = computed(() => {
|
||||
if (!authStore.isAuthenticated) return 'Not signed in'
|
||||
if (listsStore.isSyncing) return 'Syncing…'
|
||||
if (listsStore.pendingCount > 0) return `${listsStore.pendingCount} change(s) waiting to sync`
|
||||
return 'Up to date'
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header class="app-header">
|
||||
<div class="brand">
|
||||
<span class="brand-dot"></span>
|
||||
<span class="brand-seal"></span>
|
||||
<span class="brand-name">dttmr</span>
|
||||
</div>
|
||||
|
||||
<div class="status">
|
||||
<span
|
||||
class="sync-dot"
|
||||
:class="{ offline: !authStore.isAuthenticated }"
|
||||
:title="listsStore.isSyncing ? 'Syncing…' : 'Up to date'"
|
||||
></span>
|
||||
<span v-if="listsStore.pendingCount > 0" class="pending">
|
||||
{{ listsStore.pendingCount }} pending
|
||||
</span>
|
||||
<span v-if="listsStore.error" class="sync-error" :title="listsStore.error">⚠ sync error</span>
|
||||
v-if="listsStore.error"
|
||||
class="stamp stamp-danger"
|
||||
:title="listsStore.error"
|
||||
>sync error</span
|
||||
>
|
||||
<span
|
||||
v-else
|
||||
class="stamp"
|
||||
:class="stampClass"
|
||||
:title="stampTitle"
|
||||
>{{ stampLabel }}</span
|
||||
>
|
||||
<button v-if="authStore.isAuthenticated" type="button" class="logout" @click="handleLogout">
|
||||
Log out
|
||||
</button>
|
||||
@@ -46,7 +71,7 @@ async function handleLogout() {
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: calc(0.75rem + var(--safe-top)) 1rem 0.75rem;
|
||||
background: linear-gradient(180deg, rgba(10, 14, 26, 0.92) 60%, rgba(10, 14, 26, 0));
|
||||
background: linear-gradient(180deg, rgba(20, 24, 26, 0.92) 60%, rgba(20, 24, 26, 0));
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
@@ -54,17 +79,22 @@ async function handleLogout() {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-weight: 600;
|
||||
color: var(--c-heading);
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.brand-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, var(--c-accent-strong), var(--c-accent-soft));
|
||||
box-shadow: 0 0 12px var(--c-accent);
|
||||
.brand-name {
|
||||
font-family: var(--font-stamp);
|
||||
font-size: 0.95rem;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.brand-seal {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 3px;
|
||||
background: var(--c-accent);
|
||||
transform: rotate(-8deg);
|
||||
box-shadow: 0 0 0 2px var(--c-bg) inset;
|
||||
}
|
||||
|
||||
.status {
|
||||
@@ -75,18 +105,19 @@ async function handleLogout() {
|
||||
color: var(--c-text-soft);
|
||||
}
|
||||
|
||||
.sync-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--c-success);
|
||||
.stamp {
|
||||
color: var(--c-text-soft);
|
||||
}
|
||||
|
||||
.sync-dot.offline {
|
||||
background-color: var(--c-text-soft);
|
||||
.stamp-synced {
|
||||
color: var(--c-success);
|
||||
}
|
||||
|
||||
.sync-error {
|
||||
.stamp-pending {
|
||||
color: var(--c-accent-strong);
|
||||
}
|
||||
|
||||
.stamp-danger {
|
||||
color: var(--c-danger);
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
@@ -10,14 +10,23 @@ const listsStore = useListsStore()
|
||||
<template>
|
||||
<nav v-if="authStore.isAuthenticated" class="bottom-nav">
|
||||
<RouterLink to="/" class="nav-item" active-class="is-active">
|
||||
<span class="nav-icon">☰</span>
|
||||
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M4 6h1.5M4 12h1.5M4 18h1.5" stroke-linecap="round" />
|
||||
<path d="M9 6h11M9 12h11M9 18h11" stroke-linecap="round" />
|
||||
</svg>
|
||||
<span class="nav-label">Lists</span>
|
||||
<span v-if="listsStore.pendingCount > 0" class="nav-badge">{{
|
||||
<span v-if="listsStore.pendingCount > 0" class="nav-badge mono-num">{{
|
||||
listsStore.pendingCount
|
||||
}}</span>
|
||||
</RouterLink>
|
||||
<RouterLink to="/account" class="nav-item" active-class="is-active">
|
||||
<span class="nav-icon">⚙</span>
|
||||
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="3.2" />
|
||||
<path
|
||||
d="M12 3.5v2M12 18.5v2M20.5 12h-2M5.5 12h-2M17.7 6.3l-1.4 1.4M7.7 16.3l-1.4 1.4M17.7 17.7l-1.4-1.4M7.7 7.7 6.3 6.3"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
</svg>
|
||||
<span class="nav-label">Account</span>
|
||||
</RouterLink>
|
||||
</nav>
|
||||
@@ -45,14 +54,19 @@ const listsStore = useListsStore()
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.15rem;
|
||||
gap: 0.2rem;
|
||||
color: var(--c-text-soft);
|
||||
font-size: 0.7rem;
|
||||
font-size: 0.68rem;
|
||||
transition: color 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
.nav-item.is-active {
|
||||
color: var(--c-accent-strong);
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
font-size: 1.25rem;
|
||||
line-height: 1;
|
||||
width: 21px;
|
||||
height: 21px;
|
||||
}
|
||||
|
||||
.nav-badge {
|
||||
@@ -64,8 +78,9 @@ const listsStore = useListsStore()
|
||||
padding: 0 4px;
|
||||
border-radius: 999px;
|
||||
background-color: var(--c-accent);
|
||||
color: #fff;
|
||||
color: var(--c-bg);
|
||||
font-size: 0.6rem;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
@@ -126,9 +126,6 @@ async function handleSubmit() {
|
||||
}
|
||||
|
||||
.banner-success {
|
||||
background-color: rgba(52, 211, 153, 0.12);
|
||||
border: 1px solid rgba(52, 211, 153, 0.4);
|
||||
color: var(--c-success);
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
|
||||
@@ -371,7 +371,7 @@ function inviteDetail(invite: Invite): string {
|
||||
}
|
||||
|
||||
.invite-code {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.95rem;
|
||||
letter-spacing: 0.06em;
|
||||
color: var(--c-heading);
|
||||
@@ -388,7 +388,7 @@ function inviteDetail(invite: Invite): string {
|
||||
}
|
||||
|
||||
.pill-active {
|
||||
background-color: rgba(52, 211, 153, 0.14);
|
||||
background-color: var(--c-success-bg);
|
||||
color: var(--c-success);
|
||||
}
|
||||
|
||||
@@ -450,7 +450,7 @@ function inviteDetail(invite: Invite): string {
|
||||
|
||||
.ticket-btn-danger {
|
||||
color: var(--c-danger);
|
||||
border-color: rgba(248, 113, 113, 0.35);
|
||||
border-color: rgba(193, 97, 74, 0.4);
|
||||
}
|
||||
|
||||
.ticket-btn-danger:hover:not(:disabled) {
|
||||
|
||||
@@ -54,7 +54,7 @@ function handleDelete(event: Event) {
|
||||
<div class="list-card-main">
|
||||
<h3>{{ list.name }}</h3>
|
||||
<p class="meta">
|
||||
{{ completedCount }}/{{ totalCount }} done
|
||||
<span class="mono-num">{{ completedCount }}/{{ totalCount }}</span> done
|
||||
<span v-if="list.pendingSync" class="pending-tag">syncing…</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -134,22 +134,23 @@ function handleDelete(event: Event) {
|
||||
|
||||
.checkbox {
|
||||
flex-shrink: 0;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--c-border-hover);
|
||||
width: 21px;
|
||||
height: 21px;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1.5px solid var(--c-border-hover);
|
||||
background: transparent;
|
||||
color: #fff;
|
||||
color: var(--c-bg);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
font-size: 0.75rem;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.item-row.completed .checkbox {
|
||||
background: linear-gradient(135deg, var(--c-accent-strong), var(--c-accent-soft));
|
||||
border-color: transparent;
|
||||
background: var(--c-accent);
|
||||
border-color: var(--c-accent);
|
||||
}
|
||||
|
||||
.title {
|
||||
|
||||
@@ -110,9 +110,6 @@ async function handleAddUser() {
|
||||
}
|
||||
|
||||
.banner-success {
|
||||
background-color: rgba(52, 211, 153, 0.12);
|
||||
border: 1px solid rgba(52, 211, 153, 0.4);
|
||||
color: var(--c-success);
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user