Files
platform/frontend-v2/DESIGN_SYSTEM.md
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

468 lines
19 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Design System — Token Reference
> Source of truth: `src/app.css`
> Last updated: 2026-03-27
All UI values must come from these tokens unless listed under [Intentional Raw Values](#intentional-raw-values).
---
## Spacing
4px grid. Token name = multiplier (`--sp-3` = 3 × 4px = 12px).
| Token | Value | Use for |
|-------|-------|---------|
| `--sp-0` | 0px | Explicit zero |
| `--sp-px` | 1px | Borders, hairlines |
| `--sp-0.5` | 2px | Micro-nudge (margin-top on meta text) |
| `--sp-1` | 4px | Tight gap, field label gap, small padding |
| `--sp-1.5` | 6px | Badge gap, icon gap, footer gap |
| `--sp-2` | 8px | Compact gap, button group gap, inner padding |
| `--sp-3` | 12px | Standard gap, row padding, list item gap |
| `--sp-4` | 16px | Card padding (mobile), section margin, tab margin |
| `--sp-5` | 20px | Card padding (desktop), module gap, sidebar gap |
| `--sp-6` | 24px | Large padding, overlay padding, page container |
| `--sp-7` | 28px | Primary card padding, section group margin |
| `--sp-8` | 32px | Page top padding, empty state, desktop grid gap |
| `--sp-10` | 40px | Large elements (avatar width), empty list padding |
| `--sp-12` | 48px | Empty state padding, large spacing |
| `--sp-16` | 64px | Reserved |
| `--sp-20` | 80px | Page bottom padding (scroll clearance) |
### Semantic spacing aliases
| Token | Resolves to | Use for |
|-------|-------------|---------|
| `--section-gap` | `--sp-7` (28px) | Gap between major page sections |
| `--card-pad` | `--sp-5` (20px) | Default module/card padding |
| `--card-pad-primary` | `--sp-7` (28px) | Hero/primary module padding |
| `--card-pad-secondary` | `--sp-4` (16px) | Compact card padding |
| `--row-gap` | `--sp-3` (12px) | Gap between list rows |
| `--module-gap` | `--sp-5` (20px) | Gap between dashboard modules |
| `--row-pad-y` | 14px | Vertical padding inside data rows (off-grid, intentional) |
| `--row-pad-x` | `--sp-4` (16px) | Horizontal padding inside data rows |
| `--inner-gap` | `--sp-3` (12px) | Gap between items within a row |
### Mobile overrides (≤768px)
| Token | Desktop | Mobile |
|-------|---------|--------|
| `--card-pad` | 20px | 16px |
| `--card-pad-primary` | 28px | 20px |
| `--row-pad-y` | 14px | 16px |
| `--section-gap` | 28px | 20px |
---
## Radius
| Token | Value | Use for |
|-------|-------|---------|
| `--radius-xs` | 4px | Tiny pills, skeleton placeholders, kbd hints |
| `--radius-sm` | 6px | Badges, chips, nav links, danger buttons |
| `--radius-md` | 8px | Buttons, inputs, tabs, entry rows, icon containers |
| `--radius` | 12px | Cards, modals, panels, main containers |
| `--radius-lg` | 16px | Hero cards, action cards, pill chips, ImmichPicker modal |
| `--radius-full` | 9999px | Circles, toggles, avatars |
---
## Elevation (Shadows)
Light and dark mode have separate values. Dark mode uses higher opacity.
| Token | Light | Use for |
|-------|-------|---------|
| `--shadow-xs` | `0 1px 2px rgba(0,0,0,0.03)` | Row hover, active tabs, inner elements |
| `--shadow-sm` | `0 1px 3px rgba(0,0,0,0.04), 0 4px 12px rgba(0,0,0,0.04)` | Secondary cards, inputs, budget tables |
| `--shadow-md` | `0 2px 6px rgba(0,0,0,0.04), 0 8px 24px rgba(0,0,0,0.06)` | Standard card elevation (default `.module`) |
| `--shadow-lg` | `0 4px 12px rgba(0,0,0,0.06), 0 16px 40px rgba(0,0,0,0.1)` | Hero cards, primary modules, dropdowns |
| `--shadow-xl` | `0 8px 24px rgba(0,0,0,0.08), 0 24px 60px rgba(0,0,0,0.15)` | Modals, overlay panels |
**Legacy aliases**: `--card-shadow``--shadow-md`, `--card-shadow-sm``--shadow-sm`
---
## Colors
### Surfaces (3-layer depth)
| Token | Light | Dark | Layer |
|-------|-------|------|-------|
| `--canvas` | `#F5F6F8` | `#09090b` | Page background — everything sits on this |
| `--surface` | `#FFFFFF` | `#0f0f12` | Sidebars, panels, slide-out sheets |
| `--surface-secondary` | `#FAFAFB` | `#111114` | Input backgrounds, secondary panels |
| `--card` | `#FFFFFF` | `#161619` | Content containers, elevated with shadow |
| `--card-secondary` | `#FAFAFB` | `#111114` | Secondary cards, button backgrounds |
| `--card-hover` | `#f0f0f3` | `#1c1c20` | Row hover, interactive feedback |
### Borders
| Token | Light | Dark |
|-------|-------|------|
| `--border` | `rgba(0,0,0,0.06)` | `rgba(255,255,255,0.06)` |
| `--border-strong` | `rgba(0,0,0,0.1)` | `rgba(255,255,255,0.1)` |
### Text hierarchy
| Token | Light | Dark | Use for |
|-------|-------|------|---------|
| `--text-1` | `#1a1a1f` | `#fafafa` | Headings, names, amounts — read first |
| `--text-2` | `#4a4a55` | `#a1a1aa` | Body text, descriptions — read second |
| `--text-3` | `#6b6b76` | `#71717a` | Labels, metadata, captions — supporting |
| `--text-4` | `#b4b4bd` | `#3f3f46` | Placeholders, disabled, timestamps — background |
### Accent (indigo / blue)
| Token | Light | Dark | Use for |
|-------|-------|------|---------|
| `--accent` | `#4F46E5` | `#3b82f6` | Primary actions, links, active states |
| `--accent-bg` | `#EEF2FF` | `rgba(59,130,246,0.1)` | Icon wells, strong highlight backgrounds |
| `--accent-dim` | `rgba(79,70,229,0.06)` | `rgba(59,130,246,0.08)` | Subtle hover, selection backgrounds, focus rings |
| `--accent-border` | `rgba(79,70,229,0.10)` | `rgba(59,130,246,0.12)` | Accent-tinted borders |
| `--accent-focus` | `rgba(79,70,229,0.12)` | `rgba(59,130,246,0.15)` | Active states, selection bars |
### Semantic colors
| Token | Light | Dark | Use for |
|-------|-------|------|---------|
| `--success` | `#16A34A` | `#22c55e` | Positive values, income, completed |
| `--success-bg` | `#F0FDF4` | `rgba(34,197,94,0.1)` | Icon wells |
| `--success-dim` | `rgba(34,197,94,0.08)` | `rgba(34,197,94,0.08)` | Badge backgrounds |
| `--error` | `#DC2626` | `#ef4444` | Errors, issues, delete actions |
| `--error-bg` | `#FEF2F2` | `rgba(239,68,68,0.1)` | Icon wells |
| `--error-dim` | `rgba(239,68,68,0.08)` | `rgba(239,68,68,0.08)` | Badge backgrounds |
| `--warning` | `#d97706` | `#f59e0b` | Warnings, pending states |
| `--warning-bg` | `rgba(245,158,11,0.08)` | `rgba(245,158,11,0.1)` | Badge backgrounds |
### Overlay
| Token | Light | Dark | Use for |
|-------|-------|------|---------|
| `--overlay` | `rgba(0,0,0,0.3)` | `rgba(0,0,0,0.6)` | Modal backdrop, reading pane overlay |
| `--overlay-strong` | `rgba(0,0,0,0.5)` | `rgba(0,0,0,0.75)` | Heavy overlays |
| `--nav-bg` | `rgba(255,255,255,0.9)` | `rgba(15,15,18,0.9)` | Navbar blur background |
---
## Typography
| Token | Desktop | Mobile (≤768px) | Use for |
|-------|---------|-----------------|---------|
| `--text-xs` | 11px | 12px | Badges, pills, tiny counters |
| `--text-sm` | 13px | 15px | Labels, meta, captions, button text |
| `--text-base` | 14px | 16px | Body text, list items, inputs |
| `--text-md` | 15px | 17px | Card titles, important rows (16px+ avoids iOS zoom) |
| `--text-lg` | 17px | 18px | Section headers, modal titles |
| `--text-xl` | 22px | 22px | Page titles |
| `--text-2xl` | 28px | 26px | Hero headings |
| `--text-3xl` | 36px | 32px | Large hero numbers |
### Line heights
| Token | Value | Use for |
|-------|-------|---------|
| `--leading-tight` | 1.2 | Headings, hero numbers |
| `--leading-snug` | 1.35 | Card titles, compact text |
| `--leading-normal` | 1.5 | Body text |
| `--leading-relaxed` | 1.65 | Article content |
| `--leading-loose` | 1.8 | Long-form reading |
### Fonts
| Token | Value |
|-------|-------|
| `--font` | `'Inter', -apple-system, system-ui, sans-serif` |
| `--mono` | `'JetBrains Mono', ui-monospace, monospace` |
---
## Global Component Classes
Defined in `app.css`, usable in any component without local `<style>` duplication.
| Class | Description |
|-------|-------------|
| `.module` | Card container (bg, border, shadow, padding) |
| `.module.primary` | Hero card (more padding + elevation) |
| `.module.flush` | No padding (for flush content) |
| `.module-header` | Flex header row (title left, action right) |
| `.module-title` | Uppercase label |
| `.module-action` | Accent link → "View all →" |
| `.data-row` | Standard list item with hover + zebra striping |
| `.badge` + `.error/.success/.warning/.accent/.muted` | Semantic status badges |
| `.tab-bar` + `.tab` + `.tab-badge` | Pill-style tab navigation |
| `.section-label` | Uppercase group header |
| `.btn-primary` / `.btn-primary.full` | Primary action buttons |
| `.btn-secondary` | Secondary action buttons |
| `.btn-icon` | Square icon button (36×36) |
| `.input` | Standard text input |
| `.skeleton` | Shimmer loading placeholder |
| `.page` / `.page-header` / `.page-title` / `.page-greeting` | Page-level layout |
| `.app-surface` | Centered max-width container |
---
## Intentional Raw Values
These values exist outside the token system by design. Do not convert them.
### Non-scale spacing
Values that don't land on the 4px grid. Used for optical tuning where grid steps are too coarse.
| Value | Where | Why |
|-------|-------|-----|
| `1px` | `margin-top` on meta text, feed separators | Sub-pixel nudge for vertical alignment |
| `3px` | Badge padding-y, kbd padding | Optical centering within small elements |
| `5px` | Pill padding-y, chip padding | Between sp-1 (4px) and sp-1.5 (6px), tuned per element |
| `7px` | View-btn padding-y, sidebar gap | Between sp-1.5 (6px) and sp-2 (8px) |
| `9px` | Nav item padding-y, suggestion row padding-y | Between sp-2 (8px) and 10px |
| `10px` | Input padding-y, dropdown padding, various gaps | Common "comfortable touch" size, between sp-2 and sp-3 |
| `11px` | Entry row padding-y, toggle btn padding-bottom | Asymmetric optical alignment |
| `13px` | FAB action padding-y, list row padding | Between sp-3 (12px) and 14px |
| `14px` | Button padding-x, row padding-x, modal gap, field row gap | Most common off-grid value. Used as standard horizontal rhythm for interactive elements. Also `--row-pad-y` for data rows. |
| `15px` | Transaction row padding-y | Between sp-3.5 and sp-4, tuned for readability |
| `18px` | Detail header margin-bottom, AI guide padding-top | Between sp-4 (16px) and sp-5 (20px) |
| `22px` | Trip stats padding-x, modal body padding | Between sp-5 (20px) and sp-6 (24px) |
### Non-scale border-radius
| Value | Where | Why |
|-------|-------|-----|
| `9px` | Status segment control inner radius | Between radius-md (8px) and radius (12px) |
| `10px` | Event cards, photo thumbnails, notes, modals, unscheduled items, food rows | Heavily used "soft card" radius. Between radius-md (8px) and radius (12px). |
| `14px` | CommandPalette box, toggle track | Large interactive controls |
| `20px` | Bottom sheet top corners | Extra-round for sheet feel |
| `50%` | Circular elements (cover nav dots, meal-number, image-delete) | True circle, differs from radius-full on non-square elements |
### Data visualization colors
These are visual category identifiers, not semantic UI colors. They must remain consistent within their visualization set, independent of theme.
**Fitness macros:**
| Color | Hex | Use |
|-------|-----|-----|
| Protein | `#8B5CF6` | Purple macro bar |
| Carbs | `#F59E0B` | Amber macro bar |
| Fat | `#3B82F6` | Blue macro bar |
**Fitness meal weights:**
| Color | Background | Text | Meaning |
|-------|------------|------|---------|
| Heavy | `rgba(239,68,68,0.1)` | `#DC2626` | High-calorie meal |
| Moderate | `rgba(245,158,11,0.1)` | `#B45309` | Medium-calorie meal |
| Light | `rgba(34,197,94,0.1)` | `#15803D` | Low-calorie meal |
**Trip categories:**
| Color | Background | Text | Category |
|-------|------------|------|----------|
| Hotel | `rgba(168,85,247,0.1)` | `#a855f7` | Lodging |
| Restaurant | `rgba(249,115,22,0.1)` | `#f97316` | Food & dining |
| Hike | `rgba(34,197,94,0.15)` | `#16a34a` | Outdoor activity |
| Logistics | `rgba(161,161,170,0.1)` | `--text-3` | Transport, other |
**Other:**
| Color | Hex | Use |
|-------|-----|-----|
| Favorite star | `#F59E0B` | Star icon fill (reader, fitness) |
| AI badge | `rgba(59,130,246,0.1)` / `#3B82F6` | AI-logged entry indicator |
| Transfer pill | `rgba(59,130,246,0.1)` / `#3b82f6` | Budget transfer indicator |
### Glass & overlay effects
Applied to elements layered over images. Not theme-switchable because they depend on photo content, not UI surface.
| Value | Where |
|-------|-------|
| `rgba(0,0,0,0.7)..0.1` | Cover image gradient (trips) |
| `rgba(255,255,255,0.15)` | Cover nav button background |
| `rgba(255,255,255,0.3)` | Cover nav button hover |
| `rgba(0,0,0,0.3)` / `rgba(0,0,0,0.5)` | Cover share button / hover |
| `rgba(0,0,0,0.35)` | Modal overlays (trips), detail overlay (inventory) |
| `rgba(0,0,0,0.5)` | Image delete button, search saving overlay |
### Directional panel shadows
Non-standard shadow directions for slide-in panels and bottom sheets. Cannot use elevation tokens.
| Value | Where |
|-------|-------|
| `0 -8px 32px rgba(0,0,0,0.12)` | FAB bottom sheet (fitness, trips) |
| `-12px 0 40px rgba(0,0,0,0.12)` | Detail slide-in sheet (inventory) |
| `-6px 0 28px rgba(0,0,0,0.08)` | Reading pane (reader) |
| `-8px 0 32px rgba(0,0,0,0.1)` | Edit sheet (trips) |
| `8px 0 24px rgba(0,0,0,0.08)` | Mobile sidebar (reader) |
### Accent-tinted FAB shadows
Colored shadows for floating action buttons. Not in the elevation scale because they use accent color, not black.
| Value | Where |
|-------|-------|
| `0 8px 24px rgba(79,70,229,0.3)` | FAB resting (fitness) |
| `0 12px 32px rgba(79,70,229,0.4)` | FAB hover (fitness) |
| `0 6px 20px rgba(79,70,229,0.3)` | FAB resting (trips) |
| `0 8px 28px rgba(79,70,229,0.4)` | FAB hover (trips) |
### Near-match shadows
Shadows that are close to tokens but intentionally differ in blur radius or opacity.
| Value | Nearest token | Diff |
|-------|--------------|------|
| `0 2px 6px rgba(0,0,0,0.05)` | `--shadow-xs` | Larger blur, higher opacity |
| `0 1px 2px rgba(0,0,0,0.04)` | `--shadow-xs` | Opacity 0.04 vs 0.03 |
| `0 1px 4px rgba(0,0,0,0.04)` | `--shadow-xs` | Larger blur |
| `0 1px 4px rgba(0,0,0,0.08)` | `--shadow-xs` | Larger blur + opacity |
| `0 16px 48px rgba(0,0,0,0.15)` | `--shadow-xl` | Single layer vs dual |
| `0 1px 3px rgba(0,0,0,0.06)` | `--shadow-xs` | Higher opacity |
| `0 8px 24px rgba(0,0,0,0.12)` | `--shadow-md` | Higher opacity |
| `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 &rarr;</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