Merge pull request 'Redesign' (#13) from dev into main

This commit was merged in pull request #13.
This commit is contained in:
2026-08-31 22:01:48 +02:00
14 changed files with 219 additions and 115 deletions
+7 -1
View File
@@ -5,12 +5,18 @@
<link rel="icon" href="/favicon.ico"> <link rel="icon" href="/favicon.ico">
<link rel="apple-touch-icon" href="/apple-touch-icon.png"> <link rel="apple-touch-icon" href="/apple-touch-icon.png">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover"> <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<meta name="theme-color" content="#0a0e1a"> <meta name="theme-color" content="#12161d">
<meta name="color-scheme" content="dark"> <meta name="color-scheme" content="dark">
<meta name="mobile-web-app-capable" content="yes"> <meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"> <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="dttmr"> <meta name="apple-mobile-web-app-title" content="dttmr">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link
href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=IBM+Plex+Sans:wght@400;500;600;700&family=IBM+Plex+Serif:wght@500;600;700&family=Special+Elite&display=swap"
rel="stylesheet"
>
<title>dttmr</title> <title>dttmr</title>
</head> </head>
<body> <body>
+47 -37
View File
@@ -1,33 +1,40 @@
/* dttmr color palette - dark, modern, blueish */ /* dttmr palette - field-ledger / stamped ink, cool slate ground, blue ink accent */
:root { :root {
--c-bg: #0a0e1a; --c-bg: #12161d;
--c-bg-soft: #101627; --c-bg-soft: #181e27;
--c-bg-mute: #161d33; --c-bg-mute: #202632;
--c-bg-elevated: #1a2338; --c-bg-elevated: #262e3c;
--c-border: rgba(120, 150, 220, 0.16); --c-border: rgba(74, 130, 196, 0.16);
--c-border-hover: rgba(120, 150, 220, 0.32); --c-border-hover: rgba(74, 130, 196, 0.36);
--c-text: #c7d0e6; --c-text: #aab2c0;
--c-text-soft: #8892b0; --c-text-soft: #707a8c;
--c-heading: #f1f4fc; --c-heading: #eef1f6;
--c-accent: #4f7dfa; --c-accent: #3d72b4;
--c-accent-soft: #3a5fd9; --c-accent-soft: #2f5c92;
--c-accent-strong: #6c93ff; --c-accent-strong: #62a0e0;
--c-accent-bg: rgba(79, 125, 250, 0.14); --c-accent-bg: rgba(61, 114, 180, 0.16);
--c-success: #34d399; --c-success: #6ba178;
--c-danger: #f87171; --c-success-bg: rgba(107, 161, 120, 0.14);
--c-danger-bg: rgba(248, 113, 113, 0.12); --c-danger: #c1614a;
--c-warning: #fbbf24; --c-danger-bg: rgba(193, 97, 74, 0.14);
--c-warning: #cf9d3f;
--radius-sm: 8px; --font-body:
--radius-md: 12px; 'IBM Plex Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
--radius-lg: 18px; --font-display: 'IBM Plex Serif', Georgia, 'Times New Roman', serif;
--font-mono: 'IBM Plex Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
--font-stamp: 'Special Elite', var(--font-mono), monospace;
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4); --radius-sm: 3px;
--shadow-md: 0 8px 24px rgba(0, 0, 0, 0.45); --radius-md: 5px;
--radius-lg: 8px;
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.45);
--shadow-md: 0 10px 28px rgba(0, 0, 0, 0.5);
--nav-height: 60px; --nav-height: 60px;
--safe-bottom: env(safe-area-inset-bottom, 0px); --safe-bottom: env(safe-area-inset-bottom, 0px);
@@ -56,21 +63,18 @@ html {
body { body {
min-height: 100vh; min-height: 100vh;
color: var(--c-text); color: var(--c-text);
background: radial-gradient(circle at top, #101a33 0%, var(--c-bg) 55%); background-color: var(--c-bg);
background-image:
repeating-linear-gradient(
to bottom,
rgba(74, 130, 196, 0.06) 0,
rgba(74, 130, 196, 0.06) 1px,
transparent 1px,
transparent 28px
),
radial-gradient(circle at 50% -10%, rgba(74, 130, 196, 0.08), transparent 55%);
line-height: 1.55; line-height: 1.55;
font-family: font-family: var(--font-body);
Inter,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
Oxygen,
Ubuntu,
Cantarell,
'Fira Sans',
'Droid Sans',
'Helvetica Neue',
sans-serif;
font-size: 15px; font-size: 15px;
text-rendering: optimizeLegibility; text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
@@ -85,6 +89,12 @@ h4 {
font-weight: 600; font-weight: 600;
} }
h1 {
font-family: var(--font-display);
font-weight: 600;
letter-spacing: -0.01em;
}
a { a {
color: var(--c-accent-strong); color: var(--c-accent-strong);
text-decoration: none; text-decoration: none;
+17 -5
View File
@@ -29,13 +29,14 @@
} }
.btn-primary { .btn-primary {
background: linear-gradient(135deg, var(--c-accent) 0%, var(--c-accent-soft) 100%); background-color: var(--c-accent);
color: #ffffff; color: var(--c-bg);
font-weight: 600;
box-shadow: var(--shadow-sm); box-shadow: var(--shadow-sm);
} }
.btn-primary:hover:not(:disabled) { .btn-primary:hover:not(:disabled) {
background: linear-gradient(135deg, var(--c-accent-strong) 0%, var(--c-accent) 100%); background-color: var(--c-accent-strong);
} }
.btn-secondary { .btn-secondary {
@@ -52,7 +53,7 @@
.btn-danger { .btn-danger {
background-color: transparent; background-color: transparent;
color: var(--c-danger); color: var(--c-danger);
border-color: rgba(248, 113, 113, 0.35); border-color: rgba(193, 97, 74, 0.4);
} }
.btn-danger:hover:not(:disabled) { .btn-danger:hover:not(:disabled) {
@@ -113,10 +114,21 @@
.banner-error { .banner-error {
background-color: var(--c-danger-bg); background-color: var(--c-danger-bg);
border: 1px solid rgba(248, 113, 113, 0.4); border: 1px solid rgba(193, 97, 74, 0.4);
color: var(--c-danger); color: var(--c-danger);
} }
.banner-success {
background-color: var(--c-success-bg);
border: 1px solid rgba(127, 174, 120, 0.4);
color: var(--c-success);
}
.mono-num {
font-family: var(--font-mono);
font-variant-numeric: tabular-nums;
}
.page { .page {
min-height: 100vh; min-height: 100vh;
padding: 1rem 1rem calc(var(--nav-height) + var(--safe-bottom) + 1.5rem); padding: 1rem 1rem calc(var(--nav-height) + var(--safe-bottom) + 1.5rem);
+60 -25
View File
@@ -1,4 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { useAuthStore } from '@/stores/auth' import { useAuthStore } from '@/stores/auth'
import { useListsStore } from '@/stores/lists' import { useListsStore } from '@/stores/lists'
@@ -11,25 +12,49 @@ async function handleLogout() {
await authStore.logout() await authStore.logout()
router.push('/login') 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> </script>
<template> <template>
<header class="app-header"> <header class="app-header">
<div class="brand"> <div class="brand">
<span class="brand-dot"></span> <span class="brand-seal"></span>
<span class="brand-name">dttmr</span> <span class="brand-name">dttmr</span>
</div> </div>
<div class="status"> <div class="status">
<span <span
class="sync-dot" v-if="listsStore.error"
:class="{ offline: !authStore.isAuthenticated }" class="stamp stamp-danger"
:title="listsStore.isSyncing ? 'Syncing…' : 'Up to date'" :title="listsStore.error"
></span> >sync error</span
<span v-if="listsStore.pendingCount > 0" class="pending"> >
{{ listsStore.pendingCount }} pending <span
</span> v-else
<span v-if="listsStore.error" class="sync-error" :title="listsStore.error"> sync error</span> class="stamp"
:class="stampClass"
:title="stampTitle"
>{{ stampLabel }}</span
>
<button v-if="authStore.isAuthenticated" type="button" class="logout" @click="handleLogout"> <button v-if="authStore.isAuthenticated" type="button" class="logout" @click="handleLogout">
Log out Log out
</button> </button>
@@ -46,7 +71,7 @@ async function handleLogout() {
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: calc(0.75rem + var(--safe-top)) 1rem 0.75rem; 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); backdrop-filter: blur(8px);
} }
@@ -54,17 +79,22 @@ async function handleLogout() {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.5rem; gap: 0.5rem;
font-weight: 600;
color: var(--c-heading); color: var(--c-heading);
letter-spacing: 0.02em;
} }
.brand-dot { .brand-name {
width: 10px; font-family: var(--font-stamp);
height: 10px; font-size: 0.95rem;
border-radius: 50%; letter-spacing: 0.04em;
background: linear-gradient(135deg, var(--c-accent-strong), var(--c-accent-soft)); }
box-shadow: 0 0 12px var(--c-accent);
.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 { .status {
@@ -75,18 +105,23 @@ async function handleLogout() {
color: var(--c-text-soft); color: var(--c-text-soft);
} }
.sync-dot { .stamp {
display: inline-flex;
align-items: center;
gap: 0.4rem;
color: var(--c-text-soft);
}
.stamp::before {
content: '';
width: 8px; width: 8px;
height: 8px; height: 8px;
border-radius: 50%; border-radius: 50%;
background-color: var(--c-success); background-color: currentColor;
flex-shrink: 0;
} }
.sync-dot.offline { .stamp-danger {
background-color: var(--c-text-soft);
}
.sync-error {
color: var(--c-danger); color: var(--c-danger);
cursor: help; cursor: help;
} }
+23 -8
View File
@@ -10,14 +10,23 @@ const listsStore = useListsStore()
<template> <template>
<nav v-if="authStore.isAuthenticated" class="bottom-nav"> <nav v-if="authStore.isAuthenticated" class="bottom-nav">
<RouterLink to="/" class="nav-item" active-class="is-active"> <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 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 listsStore.pendingCount
}}</span> }}</span>
</RouterLink> </RouterLink>
<RouterLink to="/account" class="nav-item" active-class="is-active"> <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> <span class="nav-label">Account</span>
</RouterLink> </RouterLink>
</nav> </nav>
@@ -45,14 +54,19 @@ const listsStore = useListsStore()
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 0.15rem; gap: 0.2rem;
color: var(--c-text-soft); 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 { .nav-icon {
font-size: 1.25rem; width: 21px;
line-height: 1; height: 21px;
} }
.nav-badge { .nav-badge {
@@ -64,8 +78,9 @@ const listsStore = useListsStore()
padding: 0 4px; padding: 0 4px;
border-radius: 999px; border-radius: 999px;
background-color: var(--c-accent); background-color: var(--c-accent);
color: #fff; color: var(--c-bg);
font-size: 0.6rem; font-size: 0.6rem;
font-weight: 600;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
-3
View File
@@ -126,9 +126,6 @@ async function handleSubmit() {
} }
.banner-success { .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; margin-top: 0.75rem;
} }
+14 -5
View File
@@ -218,7 +218,7 @@ function inviteDetail(invite: Invite): string {
:class="`is-${inviteStatus(invite)}`" :class="`is-${inviteStatus(invite)}`"
> >
<div class="invite-ticket-main"> <div class="invite-ticket-main">
<code class="invite-code">{{ invite.code }}</code> <code class="invite-code" :title="invite.code">{{ invite.code }}</code>
<span class="invite-status-pill" :class="`pill-${inviteStatus(invite)}`">{{ <span class="invite-status-pill" :class="`pill-${inviteStatus(invite)}`">{{
statusLabel(invite) statusLabel(invite)
}}</span> }}</span>
@@ -368,13 +368,19 @@ function inviteDetail(invite: Invite): string {
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
gap: 0.5rem; gap: 0.5rem;
flex-wrap: wrap;
} }
.invite-code { .invite-code {
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-family: var(--font-mono);
font-size: 0.95rem; font-size: 0.95rem;
letter-spacing: 0.06em; letter-spacing: 0.06em;
color: var(--c-heading); color: var(--c-heading);
min-width: 0;
flex: 1 1 auto;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
} }
.invite-status-pill { .invite-status-pill {
@@ -388,7 +394,7 @@ function inviteDetail(invite: Invite): string {
} }
.pill-active { .pill-active {
background-color: rgba(52, 211, 153, 0.14); background-color: var(--c-success-bg);
color: var(--c-success); color: var(--c-success);
} }
@@ -406,7 +412,8 @@ function inviteDetail(invite: Invite): string {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
gap: 0.5rem; gap: 0.4rem 0.5rem;
flex-wrap: wrap;
border-top: 1px dashed var(--c-border); border-top: 1px dashed var(--c-border);
margin-top: 0.55rem; margin-top: 0.55rem;
padding-top: 0.5rem; padding-top: 0.5rem;
@@ -415,12 +422,14 @@ function inviteDetail(invite: Invite): string {
.invite-detail { .invite-detail {
font-size: 0.75rem; font-size: 0.75rem;
color: var(--c-text-soft); color: var(--c-text-soft);
min-width: 0;
} }
.invite-actions { .invite-actions {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.4rem; gap: 0.4rem;
margin-left: auto;
} }
.confirm-label { .confirm-label {
@@ -450,7 +459,7 @@ function inviteDetail(invite: Invite): string {
.ticket-btn-danger { .ticket-btn-danger {
color: var(--c-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) { .ticket-btn-danger:hover:not(:disabled) {
+1 -1
View File
@@ -54,7 +54,7 @@ function handleDelete(event: Event) {
<div class="list-card-main"> <div class="list-card-main">
<h3>{{ list.name }}</h3> <h3>{{ list.name }}</h3>
<p class="meta"> <p class="meta">
{{ completedCount }}/{{ totalCount }} done <span class="mono-num">{{ completedCount }}/{{ totalCount }}</span> done
<span v-if="list.pendingSync" class="pending-tag">syncing</span> <span v-if="list.pendingSync" class="pending-tag">syncing</span>
</p> </p>
</div> </div>
+9 -8
View File
@@ -134,22 +134,23 @@ function handleDelete(event: Event) {
.checkbox { .checkbox {
flex-shrink: 0; flex-shrink: 0;
width: 22px; width: 21px;
height: 22px; height: 21px;
border-radius: 6px; border-radius: var(--radius-sm);
border: 1px solid var(--c-border-hover); border: 1.5px solid var(--c-border-hover);
background: transparent; background: transparent;
color: #fff; color: var(--c-bg);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
cursor: pointer; cursor: pointer;
font-size: 0.75rem; font-size: 0.8rem;
font-weight: 700;
} }
.item-row.completed .checkbox { .item-row.completed .checkbox {
background: linear-gradient(135deg, var(--c-accent-strong), var(--c-accent-soft)); background: var(--c-accent);
border-color: transparent; border-color: var(--c-accent);
} }
.title { .title {
-3
View File
@@ -110,9 +110,6 @@ async function handleAddUser() {
} }
.banner-success { .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; margin-top: 0.75rem;
} }
+1 -1
View File
@@ -43,7 +43,7 @@ onMounted(() => {
<h4>Sync status</h4> <h4>Sync status</h4>
<p class="row"> <p class="row">
<span>Pending changes</span> <span>Pending changes</span>
<strong>{{ listsStore.pendingCount }}</strong> <strong class="mono-num">{{ listsStore.pendingCount }}</strong>
</p> </p>
<p class="row"> <p class="row">
<span>Syncing</span> <span>Syncing</span>
+19 -8
View File
@@ -46,7 +46,8 @@ async function handleSubmit() {
<div class="login-container"> <div class="login-container">
<div class="login-card card"> <div class="login-card card">
<div class="brand-mark"> <div class="brand-mark">
<span class="brand-dot"></span> <span class="brand-seal"></span>
<span class="brand-word">dttmr</span>
</div> </div>
<h2>Login</h2> <h2>Login</h2>
<p class="subtitle">Enter your credentials to access your account</p> <p class="subtitle">Enter your credentials to access your account</p>
@@ -119,16 +120,26 @@ async function handleSubmit() {
.brand-mark { .brand-mark {
display: flex; display: flex;
align-items: center;
justify-content: center; justify-content: center;
margin-bottom: 1rem; gap: 0.55rem;
margin-bottom: 1.5rem;
} }
.brand-dot { .brand-seal {
width: 40px; width: 30px;
height: 40px; height: 30px;
border-radius: 12px; border-radius: 6px;
background: linear-gradient(135deg, var(--c-accent-strong), var(--c-accent-soft)); background: var(--c-accent);
box-shadow: 0 0 24px var(--c-accent-bg); transform: rotate(-8deg);
box-shadow: 0 0 0 3px var(--c-bg-soft) inset;
}
.brand-word {
font-family: var(--font-stamp);
font-size: 1.3rem;
letter-spacing: 0.04em;
color: var(--c-heading);
} }
h2 { h2 {
+19 -8
View File
@@ -58,7 +58,8 @@ async function handleSubmit() {
<div class="register-container"> <div class="register-container">
<div class="register-card card"> <div class="register-card card">
<div class="brand-mark"> <div class="brand-mark">
<span class="brand-dot"></span> <span class="brand-seal"></span>
<span class="brand-word">dttmr</span>
</div> </div>
<h2>Create your account</h2> <h2>Create your account</h2>
@@ -160,16 +161,26 @@ async function handleSubmit() {
.brand-mark { .brand-mark {
display: flex; display: flex;
align-items: center;
justify-content: center; justify-content: center;
margin-bottom: 1rem; gap: 0.55rem;
margin-bottom: 1.5rem;
} }
.brand-dot { .brand-seal {
width: 40px; width: 30px;
height: 40px; height: 30px;
border-radius: 12px; border-radius: 6px;
background: linear-gradient(135deg, var(--c-accent-strong), var(--c-accent-soft)); background: var(--c-accent);
box-shadow: 0 0 24px var(--c-accent-bg); transform: rotate(-8deg);
box-shadow: 0 0 0 3px var(--c-bg-soft) inset;
}
.brand-word {
font-family: var(--font-stamp);
font-size: 1.3rem;
letter-spacing: 0.04em;
color: var(--c-heading);
} }
h2 { h2 {
+2 -2
View File
@@ -23,8 +23,8 @@ export default defineConfig({
description: 'dittmar.dev frontend app', description: 'dittmar.dev frontend app',
start_url: '/', start_url: '/',
scope: '/', scope: '/',
theme_color: '#0a0e1a', theme_color: '#12161d',
background_color: '#0a0e1a', background_color: '#12161d',
display: 'standalone', display: 'standalone',
icons: [ icons: [
{ {