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>
This commit is contained in:
@@ -322,3 +322,146 @@ Shadows that are close to tokens but intentionally differ in blur radius or opac
|
||||
| `0 20px 60px rgba(0,0,0,0.15)` | `--shadow-xl` | Single layer |
|
||||
| `0 20px 60px rgba(0,0,0,0.2)` | `--shadow-xl` | Single layer, higher opacity |
|
||||
| `0 1px 3px rgba(0,0,0,0.15)` | `--shadow-sm` | Toggle thumb, much higher opacity |
|
||||
|
||||
---
|
||||
|
||||
## Adding a New App (Frontend Guide)
|
||||
|
||||
Step-by-step for adding a new app page to the platform.
|
||||
|
||||
### 1. Create the route
|
||||
|
||||
Create `src/routes/(app)/yourapp/+page.svelte`. Every app is a single self-contained file.
|
||||
|
||||
### 2. Page structure template
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
// -- Types --
|
||||
interface Item {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
// -- State (Svelte 5 runes) --
|
||||
let loading = $state(true);
|
||||
let items = $state<Item[]>([]);
|
||||
let activeTab = $state<'all' | 'recent'>('all');
|
||||
let searchQuery = $state('');
|
||||
|
||||
// -- Derived --
|
||||
const filtered = $derived(
|
||||
items.filter(i => i.name.toLowerCase().includes(searchQuery.toLowerCase()))
|
||||
);
|
||||
|
||||
// -- API helper (scoped to this app's gateway prefix) --
|
||||
async function api(path: string, opts: RequestInit = {}) {
|
||||
const res = await fetch(`/api/yourapp${path}`, { credentials: 'include', ...opts });
|
||||
if (!res.ok) throw new Error(`${res.status}`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
// -- Data mapper (normalize backend shape to UI interface) --
|
||||
function mapItem(raw: any): Item {
|
||||
return { id: raw.id || raw.Id, name: raw.name || raw.title || '' };
|
||||
}
|
||||
|
||||
// -- Load --
|
||||
async function loadItems() {
|
||||
try {
|
||||
const data = await api('/items');
|
||||
items = (data.items || data).map(mapItem);
|
||||
} catch (e) { console.error('Failed to load items', e); }
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
await loadItems();
|
||||
loading = false;
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="page">
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Your App</h1>
|
||||
</div>
|
||||
|
||||
{#if loading}
|
||||
<div class="module"><div class="skeleton" style="height: 200px"></div></div>
|
||||
{:else}
|
||||
<div class="tab-bar">
|
||||
<button class="tab" class:active={activeTab === 'all'} onclick={() => activeTab = 'all'}>All</button>
|
||||
<button class="tab" class:active={activeTab === 'recent'} onclick={() => activeTab = 'recent'}>Recent</button>
|
||||
</div>
|
||||
|
||||
<div class="module">
|
||||
<div class="module-header">
|
||||
<span class="module-title">Items</span>
|
||||
<button class="module-action" onclick={...}>View all →</button>
|
||||
</div>
|
||||
{#each filtered as item}
|
||||
<div class="data-row">
|
||||
<span style="color: var(--text-1)">{item.name}</span>
|
||||
</div>
|
||||
{:else}
|
||||
<p style="color: var(--text-3); padding: var(--card-pad)">No items found</p>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
```
|
||||
|
||||
### 3. Register in navigation
|
||||
|
||||
**`(app)/+layout.server.ts`** -- add ID to allApps:
|
||||
```ts
|
||||
const allApps = ['trips', 'fitness', 'inventory', 'budget', 'reader', 'media', 'yourapp'];
|
||||
```
|
||||
|
||||
**`Navbar.svelte`** -- add link:
|
||||
```svelte
|
||||
{#if showApp('yourapp')}
|
||||
<a href="/yourapp" class="navbar-link" class:active={isActive('/yourapp')}>Your App</a>
|
||||
{/if}
|
||||
```
|
||||
|
||||
**`MobileTabBar.svelte`** -- add to primary tabs or the "More" sheet.
|
||||
|
||||
### 4. Key conventions
|
||||
|
||||
| Rule | Detail |
|
||||
|------|--------|
|
||||
| All state uses `$state()` | Never plain `let` for reactive values |
|
||||
| Computed values use `$derived()` | For filtered lists, counts, conditions |
|
||||
| API calls go through `/api/yourapp/*` | Gateway proxies to backend |
|
||||
| `credentials: 'include'` on every fetch | Cookie-based auth |
|
||||
| Map raw API data through `mapX()` | Normalize backend shapes to clean interfaces |
|
||||
| No shared stores or context | Each page is self-contained |
|
||||
| No separate `+page.ts` load function | Data loads in `onMount` |
|
||||
| Types defined inline in script | No shared type files |
|
||||
|
||||
### 5. UI component classes to use
|
||||
|
||||
| Class | When |
|
||||
|-------|------|
|
||||
| `.page` / `.page-header` / `.page-title` | Page-level layout |
|
||||
| `.module` / `.module.primary` / `.module.flush` | Card containers |
|
||||
| `.module-header` / `.module-title` / `.module-action` | Card headers |
|
||||
| `.data-row` | List items with hover + zebra |
|
||||
| `.badge` + `.error/.success/.warning/.accent/.muted` | Status indicators |
|
||||
| `.tab-bar` + `.tab` | Pill-style tabs |
|
||||
| `.btn-primary` / `.btn-secondary` / `.btn-icon` | Buttons |
|
||||
| `.input` | Text inputs |
|
||||
| `.skeleton` | Loading placeholders |
|
||||
| `.section-label` | Uppercase group header |
|
||||
|
||||
### 6. Styling rules
|
||||
|
||||
- All colors from tokens: `var(--text-1)`, `var(--accent)`, `var(--card)`, etc.
|
||||
- All spacing from tokens: `var(--sp-3)`, `var(--card-pad)`, `var(--row-gap)`, etc.
|
||||
- All radii from tokens: `var(--radius)`, `var(--radius-md)`, etc.
|
||||
- All shadows from tokens: `var(--shadow-md)`, `var(--shadow-lg)`, etc.
|
||||
- All font sizes from tokens: `var(--text-sm)`, `var(--text-base)`, etc.
|
||||
- Never use raw `#hex` colors, raw `px` spacing, or raw shadows unless listed in the Intentional Raw Values section above
|
||||
- Dark mode is automatic via CSS custom properties — no manual `prefers-color-scheme` needed
|
||||
|
||||
Reference in New Issue
Block a user