Files
platform/frontend-v2/src/lib/components/layout/MobileTabBar.svelte
Yusuf Suleman 6023ebf9d0 feat: tasks app, security hardening, mobile fixes, iOS app shell
- 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>
2026-03-30 15:35:57 -05:00

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>