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

19 KiB
Raw Permalink Blame History

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.


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

<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:

const allApps = ['trips', 'fitness', 'inventory', 'budget', 'reader', 'media', 'yourapp'];

Navbar.svelte -- add link:

{#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