- Custom SQLite task manager replacing TickTick wrapper - 73 tasks migrated from TickTick across 15 projects - RRULE recurrence engine with lazy materialization - Dashboard tasks widget (desktop sidebar + mobile card) - Tasks page with project tabs, add/edit/complete/delete - Security: locked ports to localhost, removed old containers - Gitea Actions runner configured and all 3 CI jobs passing - Fixed mobile overflow on dashboard cards - iOS Capacitor app shell (Second Brain) - Frontend/backend guide docs for adding new services - TickTick Google Calendar sync re-authorized Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
193 lines
4.6 KiB
Svelte
193 lines
4.6 KiB
Svelte
<script lang="ts">
|
|
import { page } from '$app/state';
|
|
import { LayoutDashboard, DollarSign, Package, Activity, MoreVertical, MapPin, BookOpen, Library, Settings, CheckSquare } from '@lucide/svelte';
|
|
|
|
interface Props {
|
|
visibleApps?: string[];
|
|
}
|
|
let { visibleApps = ['trips', 'fitness', 'inventory', 'budget', 'reader', 'media'] }: Props = $props();
|
|
|
|
function showApp(id: string): boolean {
|
|
return visibleApps.includes(id);
|
|
}
|
|
|
|
let moreOpen = $state(false);
|
|
|
|
function isActive(path: string): boolean {
|
|
if (path === '/') return page.url.pathname === '/';
|
|
return page.url.pathname.startsWith(path);
|
|
}
|
|
|
|
function closeMore() {
|
|
moreOpen = false;
|
|
}
|
|
</script>
|
|
|
|
<div class="mobile-tabbar">
|
|
<div class="mobile-tabbar-inner">
|
|
<a href="/" class="mobile-tab" class:active={isActive('/')}>
|
|
<LayoutDashboard size={22} />
|
|
Dashboard
|
|
</a>
|
|
{#if showApp('budget')}
|
|
<a href="/budget" class="mobile-tab" class:active={isActive('/budget')}>
|
|
<DollarSign size={22} />
|
|
Budget
|
|
</a>
|
|
{/if}
|
|
{#if showApp('inventory')}
|
|
<a href="/inventory" class="mobile-tab" class:active={isActive('/inventory')}>
|
|
<Package size={22} />
|
|
Inventory
|
|
</a>
|
|
{/if}
|
|
{#if showApp('fitness')}
|
|
<a href="/fitness" class="mobile-tab" class:active={isActive('/fitness')}>
|
|
<Activity size={22} />
|
|
Fitness
|
|
</a>
|
|
{/if}
|
|
<button class="mobile-tab" class:active={moreOpen} onclick={() => moreOpen = true}>
|
|
<MoreVertical size={22} />
|
|
More
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- More sheet overlay -->
|
|
{#if moreOpen}
|
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
<div class="more-sheet-overlay open" onclick={(e) => { if (e.target === e.currentTarget) closeMore(); }} onkeydown={() => {}}>
|
|
<div class="more-sheet">
|
|
<div class="more-sheet-handle"></div>
|
|
{#if showApp('tasks')}
|
|
<a href="/tasks" class="more-sheet-item" onclick={closeMore}>
|
|
<CheckSquare size={20} />
|
|
Tasks
|
|
</a>
|
|
{/if}
|
|
{#if showApp('trips')}
|
|
<a href="/trips" class="more-sheet-item" onclick={closeMore}>
|
|
<MapPin size={20} />
|
|
Trips
|
|
</a>
|
|
{/if}
|
|
{#if showApp('reader')}
|
|
<a href="/reader" class="more-sheet-item" onclick={closeMore}>
|
|
<BookOpen size={20} />
|
|
Reader
|
|
</a>
|
|
{/if}
|
|
{#if showApp('media')}
|
|
<a href="/media" class="more-sheet-item" onclick={closeMore}>
|
|
<Library size={20} />
|
|
Media
|
|
</a>
|
|
{/if}
|
|
<a href="/settings" class="more-sheet-item" onclick={closeMore}>
|
|
<Settings size={20} />
|
|
Settings
|
|
</a>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
<style>
|
|
.mobile-tabbar {
|
|
display: none;
|
|
position: fixed;
|
|
bottom: 0;
|
|
left: 0;
|
|
right: 0;
|
|
z-index: 80;
|
|
background: var(--nav-bg);
|
|
backdrop-filter: blur(20px);
|
|
-webkit-backdrop-filter: blur(20px);
|
|
border-top: 1px solid var(--border);
|
|
padding-bottom: env(safe-area-inset-bottom, 0px);
|
|
}
|
|
.mobile-tabbar-inner {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-around;
|
|
height: 56px;
|
|
max-width: 500px;
|
|
margin: 0 auto;
|
|
}
|
|
.mobile-tab {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: var(--sp-0.5);
|
|
background: none;
|
|
border: none;
|
|
color: var(--text-3);
|
|
font-size: var(--text-xs);
|
|
font-weight: 500;
|
|
padding: var(--sp-1) var(--sp-2);
|
|
border-radius: var(--radius-md);
|
|
transition: all var(--transition);
|
|
min-width: 56px;
|
|
text-decoration: none;
|
|
}
|
|
.mobile-tab.active { color: var(--accent); }
|
|
.mobile-tab:hover { color: var(--text-1); }
|
|
|
|
/* More sheet */
|
|
.more-sheet-overlay {
|
|
position: fixed;
|
|
inset: 0;
|
|
background: var(--overlay);
|
|
z-index: 90;
|
|
}
|
|
.more-sheet {
|
|
position: fixed;
|
|
bottom: 0;
|
|
left: 0;
|
|
right: 0;
|
|
z-index: 91;
|
|
background: var(--surface);
|
|
border-top-left-radius: 16px;
|
|
border-top-right-radius: 16px;
|
|
padding: var(--sp-3) var(--sp-4) var(--sp-8);
|
|
animation: slideUp 0.3s ease;
|
|
}
|
|
@keyframes slideUp {
|
|
from { transform: translateY(100%); }
|
|
to { transform: translateY(0); }
|
|
}
|
|
.more-sheet-handle {
|
|
width: 36px;
|
|
height: 4px;
|
|
border-radius: 2px;
|
|
background: var(--text-4);
|
|
margin: 0 auto var(--sp-4);
|
|
}
|
|
.more-sheet-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--sp-3);
|
|
padding: 14px var(--sp-3);
|
|
border-radius: var(--radius-sm);
|
|
font-size: var(--text-md);
|
|
font-weight: 500;
|
|
color: var(--text-1);
|
|
background: none;
|
|
border: none;
|
|
width: 100%;
|
|
text-align: left;
|
|
transition: background var(--transition);
|
|
text-decoration: none;
|
|
}
|
|
.more-sheet-item:hover { background: var(--card-hover); }
|
|
.more-sheet-item :global(svg) { color: var(--text-3); }
|
|
|
|
@media (max-width: 768px) {
|
|
.mobile-tabbar { display: block; }
|
|
}
|
|
@media (min-width: 769px) {
|
|
.mobile-tabbar { display: none !important; }
|
|
.more-sheet-overlay { display: none !important; }
|
|
}
|
|
</style>
|