Files
oikos/docs/SPEC.md
T
2026-04-26 09:14:26 +02:00

29 KiB
Raw Blame History

Oikos - Product Specification

Self-hosted family planner web app for a single household (26 people). No app store, no public access. Deployment via Docker on a private Linux server behind an Nginx reverse proxy with SSL.


Data Model

Every table: id INTEGER PRIMARY KEY, created_at TEXT, updated_at TEXT (ISO 8601).

Users

Column Type Constraint
username TEXT UNIQUE NOT NULL
display_name TEXT
password_hash TEXT bcrypt
avatar_color TEXT HEX color code
role TEXT 'admin' or 'member'

Tasks

Column Type Constraint
title TEXT NOT NULL
description TEXT
category TEXT Household, School, Shopping, Repairs, Other
priority TEXT none (default), low, medium, high, urgent
status TEXT open, in_progress, done
due_date TEXT DATE, nullable
due_time TEXT TIME, nullable
assigned_to INTEGER FK → Users
created_by INTEGER FK → Users, NOT NULL
is_recurring INTEGER 0/1
recurrence_rule TEXT iCal RRULE
parent_task_id INTEGER FK → Tasks (max 2 levels)

Shopping Lists

Column Type Constraint
name TEXT NOT NULL (e.g. "Supermarket", "Hardware store")

Shopping Items

Column Type Constraint
list_id INTEGER FK → Shopping Lists, NOT NULL
name TEXT NOT NULL
quantity TEXT e.g. "500g", "2 pieces"
category TEXT FK → Shopping Categories (by name)
is_checked INTEGER 0/1
added_from_meal INTEGER FK → Meals, nullable

Shopping Categories

Custom, household-wide category list for shopping items. Replaces the old hardcoded category set.

Column Type Constraint
id INTEGER PRIMARY KEY
name TEXT NOT NULL
sort_order INTEGER NOT NULL
created_at TEXT
updated_at TEXT

Meals

Column Type Constraint
date TEXT DATE, NOT NULL
meal_type TEXT breakfast, lunch, dinner, snack
title TEXT NOT NULL
notes TEXT
recipe_url TEXT nullable, URL to recipe
recipe_id INTEGER FK → Recipes (ON DELETE SET NULL), nullable
created_by INTEGER FK → Users, NOT NULL

Recipes

Reusable recipe cards that can be pre-filled into meal slots.

Column Type Constraint
title TEXT NOT NULL
notes TEXT
recipe_url TEXT nullable
created_by INTEGER FK → Users (CASCADE delete)

Recipe Ingredients

Column Type Constraint
recipe_id INTEGER FK → Recipes (CASCADE delete), NOT NULL
name TEXT NOT NULL
quantity TEXT
category TEXT NOT NULL (default 'Sonstiges')

Meal Ingredients

Column Type Constraint
meal_id INTEGER FK → Meals, NOT NULL
name TEXT NOT NULL
quantity TEXT
on_shopping_list INTEGER 0/1

Calendar Events

Column Type Constraint
title TEXT NOT NULL
description TEXT
start_datetime TEXT DATETIME, NOT NULL
end_datetime TEXT DATETIME
all_day INTEGER 0/1
location TEXT
color TEXT HEX
assigned_to INTEGER FK → Users
created_by INTEGER FK → Users, NOT NULL
external_calendar_id TEXT ID from external calendar
external_source TEXT local, google, apple, ics
recurrence_rule TEXT iCal RRULE
subscription_id INTEGER FK → ICS Subscriptions (CASCADE delete)
user_modified INTEGER 0/1 — prevents sync overwrite when 1
calendar_ref_id INTEGER FK → External Calendars (ON DELETE SET NULL)

External Calendars

Display metadata (name, color) for synced Google/Apple calendars. Populated automatically during sync.

Column Type Constraint
source TEXT 'google' or 'apple', NOT NULL
external_id TEXT Calendar ID from the provider, NOT NULL
name TEXT Display name from the provider, NOT NULL
color TEXT Background color from the provider (HEX)
UNIQUE (source, external_id)

Notes

Column Type Constraint
title TEXT nullable
content TEXT NOT NULL
color TEXT HEX
pinned INTEGER 0/1
created_by INTEGER FK → Users, NOT NULL

Contacts

Column Type Constraint
name TEXT NOT NULL
category TEXT Doctor, School/Nursery, Authority, Insurance, Tradesperson, Emergency, Other
phone TEXT
email TEXT
address TEXT
notes TEXT

Budget Entries

Column Type Constraint
title TEXT NOT NULL
amount REAL NOT NULL (positive = income, negative = expense)
category TEXT FK → Budget Categories (by key), NOT NULL
subcategory TEXT FK → Budget Subcategories (by key), default ''
date TEXT DATE, NOT NULL
is_recurring INTEGER 0/1
recurrence_rule TEXT iCal RRULE
recurrence_parent_id INTEGER FK → Budget Entries (generated instance points to original)
created_by INTEGER FK → Users, NOT NULL

Budget Categories

Expense and income category list, DB-backed with stable English slug keys. Predefined set (8 expense, 5 income); users can add custom categories inline from the entry modal.

Column Type Constraint
key TEXT PRIMARY KEY (stable English slug, e.g. housing)
name TEXT NOT NULL
type TEXT 'expense' or 'income'
sort_order INTEGER NOT NULL DEFAULT 0
created_at TEXT ISO 8601

Budget Subcategories

Optional subcategories scoped to an expense category. Predefined set (35 entries); users can add custom subcategories inline. Income categories have no subcategories.

Column Type Constraint
key TEXT PRIMARY KEY
category_key TEXT FK → Budget Categories (CASCADE delete), NOT NULL
name TEXT NOT NULL
sort_order INTEGER NOT NULL DEFAULT 0
created_at TEXT ISO 8601
UNIQUE (category_key, name)

Budget Recurrence Skipped

Stores instances of a recurring entry deleted by the user so they are not re-generated.

Column Type Constraint
parent_id INTEGER FK → Budget Entries, NOT NULL
month TEXT YYYY-MM, NOT NULL
PRIMARY KEY (parent_id, month)

API Tokens

Named Bearer / X-API-Key tokens for non-interactive external integrations. Admin-only creation and revocation. Token values are SHA-256-hashed at rest; the plaintext is shown only once after creation.

Column Type Constraint
id INTEGER PRIMARY KEY AUTOINCREMENT
name TEXT NOT NULL
token_hash TEXT NOT NULL UNIQUE (SHA-256)
token_prefix TEXT NOT NULL (first 8 chars, for display)
created_by INTEGER FK → Users (CASCADE delete), NOT NULL
expires_at TEXT ISO 8601, nullable
revoked_at TEXT ISO 8601, nullable
last_used_at TEXT ISO 8601, nullable
created_at TEXT ISO 8601 NOT NULL

ICS Subscriptions

External calendar feeds subscribed by users (read-only, auto-synced).

Column Type Constraint
name TEXT NOT NULL
url TEXT NOT NULL (https:// or webcal://)
color TEXT HEX, default #6366f1
shared INTEGER 0/1 — visible to all family members when 1
created_by INTEGER FK → Users (SET NULL on delete)
etag TEXT HTTP ETag for conditional fetch
last_modified TEXT HTTP Last-Modified for conditional fetch
last_sync TEXT ISO timestamp of last successful sync
created_at TEXT ISO timestamp

Sync Config

Key-value table for OAuth tokens and CalDAV credentials.

Column Type Constraint
key TEXT PRIMARY KEY
value TEXT NOT NULL

Modules

Dashboard (/)

Responsive grid: 1 column on mobile, 2 on tablet, 3 on desktop.

Widgets:

  • Greeting: "Good [morning/afternoon/evening], [Name]" + date
  • Weather: OpenWeatherMap proxy, 3-day preview, refresh every 30 min, hide widget on API error
  • Upcoming events: next 35, color-coded by person
  • Urgent tasks: priority urgent/high + due_date ≤48h
  • Today's meals: meals for the current day
  • Pinboard preview: 23 pinned notes
  • FAB (quick actions): + Task, + Event, + Shopping list item, + Note

Skeleton loading instead of spinners. Clicking any widget navigates to that module.

Tasks (/tasks)

Views:

  • List view (default): grouped by category or due date (toggleable), filter: person, priority, status
  • Kanban: columns Open → In Progress → Done, drag & drop
  • View mode persisted in localStorage; URL parameter ?view=kanban overrides (useful for tablet kiosk setups)

Features:

  • CRUD + subtasks (max 2 levels, checkbox list, progress bar)
  • Assignment to users (avatar color as indicator)
  • Priorities shown visually via color/icon
  • Recurring: automatically create next instance on completion
  • Mobile swipe: left = done, right = edit
  • Badge for overdue tasks

Shopping Lists (/shopping)

  • Multiple lists in parallel
  • Items: name, category, quantity, checkbox
  • Grouping by category (aisle logic)
  • Integration with meal plan: "Add ingredients to shopping list" transfers with source reference
  • Checked items shown with strikethrough + moved to bottom
  • "Clear list" = remove checked items only
  • Autocomplete from previous entries (local)
  • Mobile swipe: left = check/uncheck, right = delete; × delete button hidden on mobile (swipe takes over)

Meal Plan (/meals)

Weekly view (MonSun), slots: breakfast / lunch / dinner / snack.

  • Meal: title + notes + ingredient list
  • "→ Shopping list" button: transfer unchecked ingredients of the week to a selected list
  • Week navigation forward/back
  • Drag & drop between days/slots
  • Autocomplete from meal history
  • Recipe integration: Select a saved recipe from the meal modal to auto-fill title, notes, URL, and ingredients. Scale ingredient quantities by a numeric factor. Save the current meal as a new recipe with one click.
  • Customizable meal visibility: In Settings, users can toggle which meal types (breakfast, lunch, dinner, snack) are shown in the planner. Stored as household-wide preference in sync_config (key: visible_meal_types). At least one type must remain active.

Recipes (/recipes)

Reusable recipe cards linked to meal slots.

  • CRUD: title, notes, recipe link, per-ingredient category
  • Duplicate existing recipes
  • "Add to meal plan" navigates to /meals with the selected recipe pre-filled in the modal
  • REST API: GET/POST /api/v1/recipes, PUT/DELETE /api/v1/recipes/:id with ingredient sync

Calendar (/calendar)

Views: Month (default, dot indicators), Week (hour grid), Day (timeline), Agenda (list).

  • CRUD: title, description, start/end, all-day, location, color, assignment
  • Color-coding per person
  • Recurring via iCal RRULE
  • Google Calendar: OAuth 2.0, Calendar API v3, two-way sync
  • Apple Calendar: CalDAV (tsdav), two-way sync
  • ICS Subscriptions: Subscribe to any public ICS/webcal URL (e.g. public holidays, sports schedules). Per-subscription color, private/shared visibility, manual "Sync now" and automatic sync on the shared interval. RRULE events expanded into a rolling ±6/+12 month window. SSRF-protected (DNS pre-resolution), ETag/Last-Modified conditional fetch, 10 MB limit, 15 s timeout. User-edited events are protected from being overwritten (user_modified); a "Reset to original" link restores them.
  • External calendar names & colors: Google and Apple sync stores each calendar's display name and background color in the external_calendars table (migration v14). A colored event-cal-label badge appears in event popups, agenda, month, week, and day views when cal_name is present.
  • Event location: Event popup and dashboard display the location field with RFC 5545 backslash-escape normalization (\n, \,, \;, \\) via fmtLocation() in public/utils/html.js.
  • Configurable sync interval (default 15 min)
  • External events visually distinguishable
  • Conflicts: external event wins, local additions are preserved

Notes (/notes)

Masonry grid with colored sticky notes.

  • CRUD: title (optional), content, color
  • Pin → appears at top + on dashboard
  • Creator shown (avatar color)
  • Markdown-light: bold, italic, lists (regex-based)
  • Full-text search: client-side filter bar, filters instantly by title + content

Contacts (/contacts)

  • CRUD with category filter
  • Phone: tel: link, email: mailto: link
  • Address: Maps link (Google/Apple via user agent)
  • Real-time search filter
  • vCard export: each contact downloadable as .vcf (GET /api/v1/contacts/:id/vcard)
  • vCard import: upload file → client-side parser (FN, TEL, EMAIL, ADR, NOTE, CATEGORIES) → create contact

Login (/login)

Unauthenticated users are redirected here. No public registration form - admin creates users via setup wizard (setup.js) or Settings.

  • Username + password form
  • Error display for wrong credentials
  • Rate limiting: 5 attempts/min/IP, 15-min lockout
  • After successful login: redirect to dashboard

Settings (/settings)

User management and app configuration. Logged-in users only.

  • Profile: change display name, avatar color, password
  • User management (admin): create new users, edit/delete existing users, assign roles (admin/member)
  • Calendar integration: connect/disconnect Google Calendar OAuth, store Apple Calendar (CalDAV) credentials, configure sync interval; manage ICS URL subscriptions (add, delete, sync now, set color and visibility)
  • Weather: configure OpenWeatherMap location
  • Language: System (follows navigator.language), German, English, Spanish, French, Italian, Swedish, Greek, Russian, Turkish, Chinese, Japanese, Arabic, Hindi, Portuguese - via oikos-locale-picker web component; switch without page reload
  • API Tokens (admin): create named Bearer / X-API-Key tokens for external integrations; the full token value is shown only once immediately after creation; tokens can be revoked at any time; support optional expiry and track last-used timestamp
  • Tab navigation: Settings is organized in seven tabs (General, Meals, Budget, Shopping, Calendar, API Tokens, Account). Sticky tab bar, active tab persists in sessionStorage, Calendar tab auto-activates after OAuth callbacks.
  • App info: version, license

Budget (/budget)

Views:

  • Monthly overview: income vs. expenses, balance, bar chart by category (Canvas, no library)

  • Transaction list: chronological, filterable

  • CRUD: title, amount, category, subcategory, date

  • Categories: DB-backed with stable English slug keys; 8 predefined expense categories, 5 income categories; users can add custom categories inline from the entry modal

  • Subcategories: 35 predefined subcategories across expense categories; users can add custom subcategories inline; displayed alongside category in each entry's metadata line

  • Recurring entries

  • Monthly comparison (current vs. previous month)

  • CSV export includes a subcategory column and English column headers

  • API: GET /api/v1/budget/categories, GET /api/v1/budget/categories/:key/subcategories (optional ?lang= localisation), POST /api/v1/budget/categories, POST /api/v1/budget/categories/:key/subcategories


API Documentation

An OpenAPI 3.0 specification is served at /api/v1/openapi.json and /openapi.json. Append ?download=1 to download as a file. The spec covers all authenticated endpoints and can be imported into any OpenAPI-compatible client (Insomnia, Postman, etc.).

Authentication options for external integrations:

  • Session cookie: standard browser session after login
  • Bearer token: Authorization: Bearer <token> — tokens created via Settings → API Tokens (admin only)
  • X-API-Key header: X-API-Key: <token> — alternative header accepted alongside Bearer

Design System

Colors (CSS Custom Properties)

Source of truth: public/styles/tokens.css. Key values (as of v0.20.39):

Palette rationale: Warm-tinted neutral scale (#FAFAF8 → #121211) anchored by an Indigo-600 primary (#4F46E5) that harmonises with the Calendar module violet and the secondary accent. Module colors are semantically separated from severity colors — no hue shared without explicit documentation in tokens.css.

:root {
  /* Neutral canvas — warm linen/unbleached-paper atmosphere */
  --color-bg:              #F5F4F1;   /* neutral-100 */
  --color-surface:         #FFFFFF;
  --color-border:          #E8E7E2;   /* neutral-200 */
  --color-text-primary:    #1C1C1A;   /* neutral-900, 14.7:1 on bg */
  --color-text-secondary:  #6C6B67;   /* neutral-600, 5.0:1 on white */
  --color-text-tertiary:   #6A6964;   /* 4.61:1 on bg */

  /* Primary accent — Indigo (warm bias, harmonises with Calendar/violet) */
  --color-accent:           #4F46E5;  /* Indigo-600, 4.93:1 on white (AA) */
  --color-accent-hover:     #4338CA;  /* Indigo-700, 7.04:1 (AAA) */
  --color-accent-active:    #3730A3;  /* Indigo-800 */
  --color-accent-deep:      #2E2D82;  /* deep Indigo for gradients/weather */
  --color-accent-secondary: #7C5CFC;  /* violet — logo gradient, same Indigo family */
  --color-accent-light:     #EEF2FF;  /* Indigo-50 */
  --color-accent-subtle:    #E0E7FF;  /* Indigo-100 */
  --color-btn-primary:      #4338CA;  /* Indigo-700, 7.04:1 on white (AAA) */
  --color-btn-primary-hover:#3730A3;

  /* Severity — hue-separated from module colors */
  --color-success:       #15803D;     /* 4.54:1 */
  --color-warning:       #A15C0A;     /* 5.23:1 — Amber, distinct from --module-meals */
  --color-danger:        #B91C1C;     /* Red-700, 6.90:1 (AAA) */
  --color-info:          #0969DA;     /* 4.64:1 */

  /* Module accents — domain-specific, not interchangeable with severity */
  --module-dashboard:  #4F46E5;   /* Indigo — follows primary accent */
  --module-tasks:      #15803D;   /* Green — intentional share with --color-success */
  --module-calendar:   #8250DF;   /* Violet */
  --module-meals:      #C2410C;   /* Orange-700 */
  --module-shopping:   #DB2777;   /* Pink-600 — distinct from Meals/Warning */
  --module-notes:      #CA8A04;   /* Gold, 4.08:1 — icons/large-text only */
  --module-contacts:   #0969DA;   /* Blue — distinct from Indigo primary */
  --module-budget:     #0F766E;   /* Teal-700, 5.11:1 */
  --module-reminders:  #0E7490;   /* Cyan-700, WCAG AA */
  --module-settings:   #6E7781;   /* Neutral grey */

  /* Priority */
  --color-priority-medium: #A16207;  /* Amber-700, 6.3:1 — distinct from Warning+Meals */
  --color-priority-high:   #C2410C;  /* = --module-meals (documented share: "hot") */
  --color-priority-urgent: #B91C1C;  /* = --color-danger (documented share: "destructive") */

  /* Glass layer tokens */
  --glass-bg: rgba(255,255,255,0.72);
  --glass-border: rgba(255,255,255,0.55);
  --blur-2xs: blur(2px);
  --blur-md: 16px;
  --radius-glass-button: 9999px;       /* capsule */
  --ease-glass: cubic-bezier(0.34, 1.56, 0.64, 1); /* spring */

  /* Glass Vibrancy tokens (Phase 4) */
  --glass-bg-card: rgba(255,255,255,0.52);
  --glass-bg-card-hover: rgba(255,255,255,0.65);
  --glass-bg-input: rgba(255,255,255,0.48);
  --glass-bg-toolbar: rgba(255,255,255,0.58);
  --glass-tint-strength: 6%;

  /* Glass inset specular highlights */
  --glass-inset-soft:     inset 0 1px 0 rgba(255,255,255,0.18);
  --glass-inset-base:     inset 0 1px 0 rgba(255,255,255,0.20);
  --glass-inset-medium:   inset 0 1px 0 rgba(255,255,255,0.22);
  --glass-inset-elevated: inset 0 1px 0 rgba(255,255,255,0.28);
  --glass-inset-strong:   inset 0 1px 0 rgba(255,255,255,0.32);
}

/* Dark mode — Hue preserved (Indigo-400), only Lightness/Saturation adjusted.
   Private --_name tokens prevent duplication between @media and [data-theme]. */
@media (prefers-color-scheme: dark) {
  :root {
    --color-bg: #1A1A18;       /* deep warm */
    --color-surface: #222220;
    --color-border: #2C2C2A;
    --color-text-primary: #F5F4F1;
    --color-text-secondary: #AEADB0;
    --color-accent:            #818CF8;  /* Indigo-400, 6.8:1 on dark surface */
    --color-accent-hover:      #6366F1;  /* Indigo-500 */
    --color-accent-active:     #4F46E5;  /* Indigo-600 — mirrors light primary */
    --color-accent-light:      #2E2D5B;
    --color-accent-subtle:     #252255;
    --color-btn-primary:       #6366F1;  /* 5.5:1 */
    --color-btn-primary-hover: #4F46E5;
    --module-dashboard: #818CF8;
    --module-meals:     #FB923C;  /* Orange-400 */
    --module-shopping:  #F472B6;  /* Pink-400 — mirrors light entanglement */
    --module-budget:    #2DD4BF;  /* Teal-400 */
    --module-reminders: #22D3EE;  /* Cyan-400 */
    --meal-dinner:      #818CF8;
    --glass-bg: rgba(28,28,26,0.75);
    --glass-border: rgba(255,255,255,0.12);
    --glass-bg-card: rgba(38,38,36,0.50);
    --glass-tint-strength: 8%;
  }
}

Typography

  • System font stack, headings 600700
  • Body: 16px mobile, 15px desktop, line-height 1.5
  • Caption: 13px, var(--color-text-secondary)

Glass Layer (public/styles/glass.css)

Additive CSS file loaded globally after layout.css. Implements a Liquid Glass design language inspired by Apple's iOS 26 Liquid Glass, adapted for CSS/web:

Phase 1-3 (Shell + Components + Polish):

  • Translucent surfaces: backdrop-filter: blur() on bottom nav, sidebar, modal overlay, cards on hover. All blur effects are inside @supports (backdrop-filter: blur(1px)) for progressive enhancement.
  • Glass tokens: Section 16 of tokens.css defines --glass-bg*, --glass-border*, --blur-2xs through --blur-xl, --opacity-glass-*, --glass-highlight*, --glass-shadow-sm/md/lg, --radius-glass-card/inner/chip/button, --ease-glass, --transition-glass. Full dark mode overrides.
  • Capsule shapes: Buttons, FAB, and search inputs use --radius-glass-button (pill shape).
  • Spring animations: Modal entrance (glass-modal-scale-in / glass-sheet-in), page transitions, and list stagger all use cubic-bezier(0.34, 1.56, 0.64, 1) spring easing.
  • FAB attention pulse: fab-ring-pulse keyframe expands a ring around the FAB to signal readiness.
  • Nav auto-hide: Bottom bar hides on scroll-down, reappears on scroll-up (mobile only, < 1024px, 4 px hysteresis). CSS: .nav-bottom--hidden { transform: translateY(calc(100% + var(--safe-area-inset-bottom))); }. JS: initNavHideOnScroll() in router.js.

Phase 4 (Vibrancy + Tint):

  • Deeper glass penetration: Dashboard widgets, task cards, note items, meal slots, form inputs, toolbars, group toggles, and FAB speed-dial actions all use semi-transparent glass backgrounds (--glass-bg-card, 52% opacity) with backdrop-filter: blur() saturate() so underlying content shines through.
  • Module tint: Each glass surface receives a subtle accent color gradient overlay via ::after pseudo-element using color-mix(in srgb, var(--module-accent) var(--glass-tint-strength), transparent). Strength is 6% in light mode, 8% in dark mode.
  • App vibrancy background: app-content uses a radial gradient with the active module accent at 3% opacity to provide an ambient color base that glass elements refract.
  • Load-order safety: All Phase 4 glass selectors use parent-scoped specificity (.dashboard .widget, .tasks-page .task-card, .meals-page .meal-slot) to prevent override by on-demand page CSS that loads after glass.css.

Accessibility: prefers-reduced-transparency, prefers-reduced-motion, and prefers-contrast: more blocks deactivate blur/animation and restore solid fallbacks across all phases.

Components

  • Cards: var(--color-surface) base, glass vibrancy via var(--glass-bg-card) + backdrop-filter: blur(8px) saturate(180%) when supported. var(--radius-md), var(--shadow-sm). Module tint overlay via ::after. Consistent padding var(--space-4) (16px) across all modules.
  • Buttons: Primary = accent + white. Secondary = outline. Min-height 44px. Capsule shape via --radius-glass-button. Submit buttons show success (checkmark, 700ms green via .btn--success) and error (shake via .btn--shaking).
  • Inputs: var(--radius-sm), 1.5px border, padding 12px 16px. Search inputs use --radius-glass-button and --glass-border-subtle. [required] fields receive validation status on blur (.form-field--error / .form-field--valid). Enter moves focus to the next field; Enter on the last field triggers submit.
  • FAB (Floating Action Button): Color follows the module accent token (--module-accent) - each module defines its own accent color. Specular inner highlight + attention ring pulse. Hidden when the virtual keyboard is open (visualViewport.resize, threshold 75% of window height).
  • Module accent colors: --module-accent is applied on three visual layers - (1) active nav tab (bottom bar + sidebar stripe), (2) toolbar border-top: 3px, (3) cards/rows border-left: 3px. The active accent is written to --active-module-accent on :root on every navigation change. Falls back to --color-accent for pages without a module context.
  • Navigation: Bottom tab bar on mobile (Dashboard, Tasks, Calendar, Meals, More), auto-hides on scroll-down. Sidebar on desktop. Both use glass blur surface.
  • Transitions: Directional slide-X animation on page change (forward = from right, back = from left, 200ms) with spring easing. Respects prefers-reduced-motion.
  • Empty states: Consistent .empty-state class across all modules (icon + title + description, centered). Compact variant .empty-state--compact for meal slots.
  • Modals: Centered panel on desktop with glass overlay. On mobile (< 768px) bottom sheet - spring slide-in from below, sheet handle visible, swipe-to-close (> 80px downward). focusin scrolls inputs into view when the virtual keyboard is open.
  • List animation: Staggered spring fade-in on load (stagger() from public/utils/ux.js) - max 5 elements staggered (30ms gap), rest appear immediately.
  • Vibration: vibrate() from public/utils/ux.js - short pulses for light actions (10-40ms), pattern [30, 50, 30] for destructive actions (delete). Respects prefers-reduced-motion.
  • PWA install prompt: Appears only after 2 user interactions. Dismiss window 7 days; interaction counter resets after dismiss.
  • PWA offline fallback: Service worker serves /offline.html when the network is unreachable and index.html is not cached. Includes a reload button.

Breakpoints

  • Mobile: < 768px (1 column, bottom nav)
  • Tablet: 7681024px (2 columns, bottom nav)
  • Desktop: > 1024px (sidebar + content)

Internationalization (i18n)

All UI strings are managed via public/i18n.js. No hardcoded text in JS files outside of locale files.

Architecture

  • Module: public/i18n.js - exports: initI18n(), setLocale(), t(key, params?), getLocale(), getSupportedLocales(), formatDate(date), formatTime(date)
  • Locale files: public/locales/de.json (reference), public/locales/en.json, public/locales/es.json, public/locales/fr.json, public/locales/it.json, public/locales/sv.json, public/locales/el.json, public/locales/ru.json, public/locales/tr.json, public/locales/zh.json, public/locales/ja.json, public/locales/ar.json, public/locales/hi.json, public/locales/pt.json - structure: { "module.camelCaseKey": "Value" }
  • Variables: {{variable}} syntax in translation strings, e.g. t('tasks.assignedTo', { name: 'Anna' })
  • Fallback chain: active locale → German (de) → key itself
  • Date format: Intl.DateTimeFormat with current locale - use formatDate() and formatTime() from i18n.js

Language Detection

  1. localStorage entry oikos-locale (manual selection)
  2. navigator.languages[0] (browser language)
  3. Fallback: en

Supported Languages

Code Language Status
de German Reference locale (all keys defined here)
en English Full translation
es Spanish Full translation
fr French Full translation (added v0.16.3)
it Italian Full translation (added v0.5.8)
sv Swedish Full translation (added v0.11.3)
el Greek Full translation (added v0.16.3)
ru Russian Full translation (added v0.16.3)
tr Turkish Full translation (added v0.16.3)
zh Chinese (Simplified) Full translation (added v0.16.3)
ja Japanese Full translation (added v0.19.0)
ar Arabic Full translation (added v0.19.0)
hi Hindi Full translation (added v0.19.0)
pt Portuguese Full translation (added v0.19.0)

Adding a New Language

  1. Create public/locales/xx.json (copy of de.json, translate)
  2. Add 'xx' to SUPPORTED_LOCALES in public/i18n.js
  3. Add label in oikos-locale-picker (LOCALE_LABELS['xx'] = 'Name')

Locale Switching

setLocale(locale) saves the selection, loads the new locale file, and fires the locale-changed custom event. All page modules and web components listen to this event and re-render - no page reload required.