Merge branch 'main' of github.com:rafaelfoster/oikos
This commit is contained in:
@@ -7,6 +7,59 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.25.2] - 2026-04-26
|
||||
|
||||
### Changed
|
||||
- Docs: `SPEC.md` updated to reflect all changes since v0.24.0 — Budget Entries table now documents `subcategory` column and DB-backed `category` FK; new `Budget Categories`, `Budget Subcategories`, and `API Tokens` data-model tables added; Settings section updated with API Tokens tab, corrected language list (added Japanese, Arabic, Hindi, Portuguese), and tab count (six → seven); Budget module section now covers subcategories, custom categories, and all new endpoints; new API Documentation section documents OpenAPI 3.0 spec and authentication options; design tokens `--blur-2xs` and `--module-reminders` added to Colors section
|
||||
- Docs: `README.md` Highlights updated — Budget Tracking now mentions DB-backed subcategories; new API Tokens entry added
|
||||
|
||||
## [0.25.1] - 2026-04-26
|
||||
|
||||
### Changed
|
||||
- Dashboard: empty widget states now render as a compact inline row (icon + text) instead of a centred column, saving ~40px of vertical space per empty widget on mobile
|
||||
|
||||
### Fixed
|
||||
- Dashboard: widget body bottom padding increased from 12px to 16px for slightly more breathing room
|
||||
- Dashboard: widget reordering in "Anpassen" modal now uses the View Transition API for smooth animations; respects `prefers-reduced-motion`
|
||||
|
||||
## [0.25.0] - 2026-04-25
|
||||
|
||||
### Added
|
||||
- API token authentication: admins can create named Bearer / X-API-Key tokens for external integrations; tokens are SHA-256-hashed at rest, support optional expiry and revocation, and track last-used timestamp
|
||||
- Settings: new "API Tokens" section for admins to create and revoke tokens; the full token value is shown only once immediately after creation
|
||||
- OpenAPI 3.0 specification served at `/api/v1/openapi.json` and `/openapi.json` (download via `?download=1`)
|
||||
- Budget: new endpoints `GET /api/v1/budget/categories` and `GET /api/v1/budget/categories/:key/subcategories` with optional `?lang=` localisation
|
||||
|
||||
### Changed
|
||||
- `server/logger.js` now serialises `Error` objects into structured JSON fields (name, message, stack) instead of logging `{}`
|
||||
|
||||
## [0.24.4] - 2026-04-26
|
||||
|
||||
### Added
|
||||
- Accessibility: `layout.css` now has a `@media (prefers-contrast: more)` block — ghost and secondary buttons get explicit borders, cards lose decorative shadows, form inputs get a 2px border, focus rings become thicker (3px, 4px offset), and active nav items get an underline as a colour-independent indicator
|
||||
|
||||
### Fixed
|
||||
- Design tokens: corrected `--sidebar-width-expanded` comment from `1280px+` to `1440px+` to match the actual breakpoint in `layout.css`
|
||||
|
||||
## [0.24.3] - 2026-04-26
|
||||
|
||||
### Added
|
||||
- Design tokens: `--blur-2xs: blur(2px)` added to the blur scale — fills the gap below `--blur-xs` (4px), used for subtle overlay blurs
|
||||
- Design tokens: `--module-reminders: #0E7490` (Cyan-700, WCAG AA) added for the reminders feature; dark mode variant `#22D3EE` (Cyan-400)
|
||||
|
||||
### Fixed
|
||||
- Design tokens: hardcoded `blur(16px)`, `blur(2px)`, and `blur(12px)` in `layout.css` replaced with `var(--blur-md)`, `var(--blur-2xs)`, and `var(--blur-sm)` — `prefers-reduced-transparency` now correctly disables all backdrop-filter effects including bottom nav, more-sheet backdrop, and sticky headers
|
||||
- Accessibility: `layout.css` now has a `prefers-reduced-transparency` block for `.nav-bottom`, `.more-backdrop`, and `.sticky-header` — these three elements previously kept their backdrop-filter active even when the user requested reduced transparency
|
||||
- Reminders: reminder bell icon in toasts now uses `var(--module-reminders)` instead of the generic `var(--color-accent)`
|
||||
|
||||
## [0.24.2] - 2026-04-26
|
||||
|
||||
### Fixed
|
||||
- Design tokens: added missing `--shadow-xl` and `--shadow-xs` tokens (with dark mode variants) — resolves undefined CSS custom property references in kanban drag ghost and dashboard widget toggle
|
||||
- Design tokens: `--color-surface-raised` replaced with `--color-surface-hover` in `dashboard.css` — was undefined, causing unstyled hover states in the widget customizer
|
||||
- Design tokens: `--color-text` replaced with `--color-text-primary` in `dashboard.css` — was undefined, causing invisible text on hover in the widget customizer
|
||||
- Design tokens: hardcoded `font-weight` values (`700`, `500`, `600`) in `reminders.css` replaced with `--font-weight-bold`, `--font-weight-medium`, `--font-weight-semibold`
|
||||
|
||||
## [0.24.1] - 2026-04-25
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -53,10 +53,12 @@
|
||||
|
||||
**Calendar Sync:** Two-way sync with Google Calendar (OAuth) and Apple iCloud (CalDAV); subscribe to any public ICS/webcal URL with per-subscription color, private/shared visibility, and automatic sync
|
||||
|
||||
**Budget Tracking:** Income and expenses, recurring entries, configurable currency (15 currencies), monthly trends, CSV export
|
||||
**Budget Tracking:** Income and expenses, recurring entries, DB-backed expense categories with subcategories (35 predefined + custom), configurable currency (15 currencies), monthly trends, CSV export
|
||||
|
||||
**Notes & Contacts:** Colored sticky notes with Markdown, contact directory with vCard import/export
|
||||
|
||||
**API Tokens:** Admins can create named Bearer / X-API-Key tokens for external integrations; tokens are SHA-256-hashed at rest, support optional expiry and revocation. OpenAPI 3.0 specification available at `/api/v1/openapi.json`.
|
||||
|
||||
**Zero Build Step:** Pure ES modules, no bundler, no transpiler, no framework. Ships what you write.
|
||||
|
||||
**Privacy First:** SQLCipher AES-256 encrypted database, fully self-hosted, zero telemetry
|
||||
|
||||
+63
-6
@@ -150,13 +150,37 @@ Display metadata (name, color) for synced Google/Apple calendars. Populated auto
|
||||
|--------|------|-----------|
|
||||
| title | TEXT | NOT NULL |
|
||||
| amount | REAL | NOT NULL (positive = income, negative = expense) |
|
||||
| category | TEXT | Groceries, Rent, Insurance, Transport, Leisure, Clothing, Health, Education, Other |
|
||||
| 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.
|
||||
|
||||
@@ -166,6 +190,21 @@ Stores instances of a recurring entry deleted by the user so they are not re-gen
|
||||
| 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).
|
||||
|
||||
@@ -307,8 +346,9 @@ User management and app configuration. Logged-in users only.
|
||||
- **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 - via `oikos-locale-picker` web component; switch without page reload
|
||||
- **Tab navigation:** Settings is organized in six tabs (General, Meals, Budget, Shopping, Calendar, Account). Sticky tab bar, active tab persists in sessionStorage, Calendar tab auto-activates after OAuth callbacks.
|
||||
- **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`)
|
||||
@@ -317,10 +357,24 @@ User management and app configuration. Logged-in users only.
|
||||
- Monthly overview: income vs. expenses, balance, bar chart by category (Canvas, no library)
|
||||
- Transaction list: chronological, filterable
|
||||
|
||||
- CRUD: title, amount, category, date
|
||||
- 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
|
||||
- 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
|
||||
|
||||
---
|
||||
|
||||
@@ -368,6 +422,7 @@ Source of truth: `public/styles/tokens.css`. Key values (as of v0.20.39):
|
||||
--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 */
|
||||
@@ -378,6 +433,7 @@ Source of truth: `public/styles/tokens.css`. Key values (as of v0.20.39):
|
||||
/* 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 */
|
||||
@@ -417,6 +473,7 @@ Source of truth: `public/styles/tokens.css`. Key values (as of v0.20.39):
|
||||
--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);
|
||||
@@ -437,7 +494,7 @@ Additive CSS file loaded globally after `layout.css`. Implements a Liquid Glass
|
||||
|
||||
**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-xs` 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.
|
||||
- **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.
|
||||
|
||||
@@ -47,5 +47,5 @@
|
||||
2. **Lack of Visual Feedback in Customization**: Reordering widgets in the customize modal (`rebuildList()`) happens instantly without transition, feeling jarring.
|
||||
|
||||
### Implementation Steps
|
||||
- [ ] **Compact Empty States (`dashboard.js`)**: Offen — `.widget__empty` hat bereits reduziertes Padding (`space-5`), aber kein echtes Row-Layout. Niedrige Priorität.
|
||||
- [ ] **Animate Widget Reordering (`dashboard.js`)**: Offen — View Transition API wäre sinnvoll, aber kein Bug. Niedrige Priorität.
|
||||
- [x] **Compact Empty States (`dashboard.css`)**: `.widget__empty` auf horizontales Row-Layout umgestellt, Icon 28→20px, Padding reduziert — spart ~40px vertikalen Platz pro leerem Widget.
|
||||
- [x] **Animate Widget Reordering (`dashboard.js`)**: `rebuildList()` nutzt nun `document.startViewTransition()` mit `prefers-reduced-motion`-Guard und `view-transition-name` je Row.
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "oikos",
|
||||
"version": "0.24.1",
|
||||
"version": "0.25.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "oikos",
|
||||
"version": "0.24.1",
|
||||
"version": "0.25.2",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bcrypt": "^6.0.0",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "oikos",
|
||||
"version": "0.24.1",
|
||||
"version": "0.25.2",
|
||||
"description": "Self-hosted family planner - calendar, tasks, shopping, meal planning, budget and more. Private, open-source, no subscription.",
|
||||
"main": "server/index.js",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
window.ui = window.SwaggerUIBundle({
|
||||
url: '/openapi.json',
|
||||
dom_id: '#swagger-ui',
|
||||
deepLinking: true,
|
||||
docExpansion: 'list',
|
||||
persistAuthorization: true,
|
||||
displayRequestDuration: true,
|
||||
filter: true,
|
||||
});
|
||||
});
|
||||
@@ -1,37 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Oikos API Docs</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui.css">
|
||||
<style>
|
||||
body { margin: 0; background: #f6f8fb; }
|
||||
.docs-topbar {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 14px 20px;
|
||||
background: #0f172a;
|
||||
color: #fff;
|
||||
font: 14px/1.4 system-ui, sans-serif;
|
||||
}
|
||||
.docs-links { display: flex; gap: 12px; flex-wrap: wrap; }
|
||||
.docs-links a { color: #93c5fd; text-decoration: none; }
|
||||
.docs-links a:hover { text-decoration: underline; }
|
||||
</style>
|
||||
<script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui-bundle.js" defer></script>
|
||||
<script src="/doc-assets/swagger-init.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<header class="docs-topbar">
|
||||
<strong>Oikos API Documentation</strong>
|
||||
<nav class="docs-links">
|
||||
<a href="/openapi.json" target="_blank" rel="noreferrer">openapi.json</a>
|
||||
<a href="/openapi.json?download=1">Download JSON</a>
|
||||
</nav>
|
||||
</header>
|
||||
<div id="swagger-ui"></div>
|
||||
</body>
|
||||
</html>
|
||||
+22
-22
@@ -631,28 +631,28 @@
|
||||
"currencyLabel": "Währung",
|
||||
"currencyHint": "Legt die Währung für den gesamten Budget-Bereich fest.",
|
||||
"currencySaved": "Währung gespeichert.",
|
||||
"apiTokensTitle": "API Tokens",
|
||||
"apiTokensCardTitle": "Access Tokens",
|
||||
"apiTokensHint": "Create API tokens for external integrations. The full token is shown only once after creation.",
|
||||
"apiTokenNameLabel": "Token name",
|
||||
"apiTokenExpiresLabel": "Expiration date",
|
||||
"apiTokenExpiresHint": "Leave empty to create a token without expiration.",
|
||||
"apiTokenCreatedLabel": "New API token",
|
||||
"apiTokenCreatedHint": "Store this token securely. It cannot be shown again.",
|
||||
"apiTokenCreate": "Create token",
|
||||
"apiTokenInvalidExpiration": "Please enter a valid expiration date.",
|
||||
"apiTokenCreatedToast": "API token created.",
|
||||
"apiTokenRevokedToast": "API token revoked.",
|
||||
"apiTokenRevokeConfirm": "Revoke API token \"{{name}}\"?",
|
||||
"apiTokenRevoke": "Revoke token",
|
||||
"apiTokenRevoked": "Revoked",
|
||||
"apiTokenExpired": "Expired",
|
||||
"apiTokenActive": "Active",
|
||||
"apiTokenPrefix": "Prefix",
|
||||
"apiTokenExpires": "Expires",
|
||||
"apiTokenNeverExpires": "No expiration",
|
||||
"apiTokenLastUsed": "Last used",
|
||||
"apiTokenNeverUsed": "Never used",
|
||||
"apiTokensTitle": "API-Tokens",
|
||||
"apiTokensCardTitle": "Zugriffstoken",
|
||||
"apiTokensHint": "Erstelle API-Tokens für externe Integrationen. Der vollständige Token wird nach der Erstellung nur einmal angezeigt.",
|
||||
"apiTokenNameLabel": "Tokenname",
|
||||
"apiTokenExpiresLabel": "Ablaufdatum",
|
||||
"apiTokenExpiresHint": "Leer lassen, um einen Token ohne Ablaufdatum zu erstellen.",
|
||||
"apiTokenCreatedLabel": "Neuer API-Token",
|
||||
"apiTokenCreatedHint": "Speichere diesen Token sicher. Er kann nicht erneut angezeigt werden.",
|
||||
"apiTokenCreate": "Token erstellen",
|
||||
"apiTokenInvalidExpiration": "Bitte gib ein gültiges Ablaufdatum ein.",
|
||||
"apiTokenCreatedToast": "API-Token erstellt.",
|
||||
"apiTokenRevokedToast": "API-Token widerrufen.",
|
||||
"apiTokenRevokeConfirm": "API-Token \"{{name}}\" widerrufen?",
|
||||
"apiTokenRevoke": "Token widerrufen",
|
||||
"apiTokenRevoked": "Widerrufen",
|
||||
"apiTokenExpired": "Abgelaufen",
|
||||
"apiTokenActive": "Aktiv",
|
||||
"apiTokenPrefix": "Präfix",
|
||||
"apiTokenExpires": "Läuft ab",
|
||||
"apiTokenNeverExpires": "Kein Ablaufdatum",
|
||||
"apiTokenLastUsed": "Zuletzt verwendet",
|
||||
"apiTokenNeverUsed": "Nie verwendet",
|
||||
"ics": {
|
||||
"title": "ICS-Abonnements",
|
||||
"add": "Abonnement hinzufügen",
|
||||
|
||||
@@ -546,7 +546,7 @@ function openCustomizeModal(currentConfig, onSave) {
|
||||
const isFirst = i === 0;
|
||||
const isLast = i === draft.length - 1;
|
||||
return `
|
||||
<div class="customize-row" data-id="${w.id}">
|
||||
<div class="customize-row" data-id="${esc(w.id)}" style="view-transition-name: widget-row-${esc(w.id)}">
|
||||
<label class="customize-row__toggle">
|
||||
<input type="checkbox" class="customize-row__check" data-id="${w.id}"
|
||||
${w.visible ? 'checked' : ''} aria-label="${widgetLabel(w.id)}">
|
||||
@@ -584,10 +584,18 @@ function openCustomizeModal(currentConfig, onSave) {
|
||||
function rebuildList() {
|
||||
const list = panel.querySelector('#customize-list');
|
||||
if (!list) return;
|
||||
list.replaceChildren();
|
||||
list.insertAdjacentHTML('beforeend', buildRows());
|
||||
if (window.lucide) window.lucide.createIcons({ el: list });
|
||||
wireRows();
|
||||
const doRebuild = () => {
|
||||
list.replaceChildren();
|
||||
list.insertAdjacentHTML('beforeend', buildRows());
|
||||
if (window.lucide) window.lucide.createIcons({ el: list });
|
||||
wireRows();
|
||||
};
|
||||
const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
if (document.startViewTransition && !reducedMotion) {
|
||||
document.startViewTransition(doRebuild);
|
||||
} else {
|
||||
doRebuild();
|
||||
}
|
||||
}
|
||||
|
||||
function wireRows() {
|
||||
|
||||
+11
-11
@@ -267,25 +267,25 @@
|
||||
|
||||
.widget__body {
|
||||
flex: 1;
|
||||
padding: var(--space-2) var(--space-4) var(--space-3);
|
||||
padding: var(--space-2) var(--space-4) var(--space-4);
|
||||
}
|
||||
|
||||
.widget__empty {
|
||||
padding: var(--space-5) var(--space-4);
|
||||
text-align: center;
|
||||
padding: var(--space-3) var(--space-4);
|
||||
color: var(--color-text-tertiary);
|
||||
font-size: var(--text-sm);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: var(--space-1);
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.widget__empty .empty-state__icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
flex-shrink: 0;
|
||||
color: var(--color-text-tertiary);
|
||||
margin-bottom: var(--space-1);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* Widget hover lift (desktop) */
|
||||
@@ -1125,7 +1125,7 @@
|
||||
}
|
||||
|
||||
.customize-row:hover {
|
||||
background-color: var(--color-surface-raised);
|
||||
background-color: var(--color-surface-hover);
|
||||
}
|
||||
|
||||
.customize-row__toggle {
|
||||
@@ -1212,8 +1212,8 @@
|
||||
}
|
||||
|
||||
.customize-row__btn:hover:not(:disabled) {
|
||||
background-color: var(--color-surface-raised);
|
||||
color: var(--color-text);
|
||||
background-color: var(--color-surface-hover);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.customize-row__btn:disabled {
|
||||
|
||||
@@ -142,8 +142,8 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
z-index: var(--z-nav);
|
||||
backdrop-filter: blur(16px) saturate(180%);
|
||||
-webkit-backdrop-filter: blur(16px) saturate(180%);
|
||||
backdrop-filter: var(--blur-md) saturate(180%);
|
||||
-webkit-backdrop-filter: var(--blur-md) saturate(180%);
|
||||
}
|
||||
|
||||
/* ── Items-Reihe ── */
|
||||
@@ -171,8 +171,8 @@
|
||||
inset: 0;
|
||||
background-color: var(--color-overlay);
|
||||
z-index: calc(var(--z-nav) + 1);
|
||||
backdrop-filter: blur(2px);
|
||||
-webkit-backdrop-filter: blur(2px);
|
||||
backdrop-filter: var(--blur-2xs);
|
||||
-webkit-backdrop-filter: var(--blur-2xs);
|
||||
}
|
||||
|
||||
.more-backdrop--visible {
|
||||
@@ -1583,8 +1583,8 @@
|
||||
top: 0;
|
||||
z-index: var(--z-sticky);
|
||||
background-color: color-mix(in srgb, var(--color-bg) 90%, transparent);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
backdrop-filter: var(--blur-sm);
|
||||
-webkit-backdrop-filter: var(--blur-sm);
|
||||
padding-bottom: var(--space-3);
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
@@ -1906,6 +1906,31 @@
|
||||
border-radius: 0 var(--radius-md) var(--radius-md) 0;
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------
|
||||
* Reduced Transparency — layout.css Fallbacks
|
||||
* glass.css deckt Glass-Komponenten ab; diese drei Elemente
|
||||
* nutzen backdrop-filter außerhalb von @supports und brauchen
|
||||
* eigene Fallbacks.
|
||||
* -------------------------------------------------------- */
|
||||
@media (prefers-reduced-transparency: reduce) {
|
||||
.nav-bottom {
|
||||
background-color: var(--color-surface);
|
||||
backdrop-filter: none;
|
||||
-webkit-backdrop-filter: none;
|
||||
}
|
||||
|
||||
.more-backdrop {
|
||||
backdrop-filter: none;
|
||||
-webkit-backdrop-filter: none;
|
||||
}
|
||||
|
||||
.sticky-header {
|
||||
background-color: var(--color-bg);
|
||||
backdrop-filter: none;
|
||||
-webkit-backdrop-filter: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------
|
||||
* Print-Styles
|
||||
* -------------------------------------------------------- */
|
||||
@@ -2000,6 +2025,52 @@ button svg { pointer-events: none; }
|
||||
/* Textarea: vertikale Größenänderung ist nutzbar */
|
||||
textarea.input { resize: vertical; }
|
||||
|
||||
/* --------------------------------------------------------
|
||||
* High Contrast (prefers-contrast: more)
|
||||
* Tokens in tokens.css stellen bereits opake Glass-Werte bereit.
|
||||
* Hier kommen komponentenspezifische Korrekturen hinzu.
|
||||
* -------------------------------------------------------- */
|
||||
@media (prefers-contrast: more) {
|
||||
/* Fokus: dicker und weiter versetzt */
|
||||
:focus-visible {
|
||||
outline-width: 3px;
|
||||
outline-offset: 4px;
|
||||
}
|
||||
|
||||
/* Ghost- und Secondary-Buttons: explizite Umrandung */
|
||||
.btn--ghost,
|
||||
.btn--secondary {
|
||||
border: 1.5px solid currentColor;
|
||||
}
|
||||
|
||||
.btn--ghost:hover {
|
||||
background-color: var(--color-surface-3);
|
||||
}
|
||||
|
||||
/* Karten: Schatten entfernen, Border verstärken */
|
||||
.card {
|
||||
box-shadow: none;
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.card--flat {
|
||||
border-color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
/* Formulareingaben: kräftigere Border */
|
||||
.input,
|
||||
.form-input {
|
||||
border-color: var(--color-text-primary);
|
||||
border-width: 2px;
|
||||
}
|
||||
|
||||
/* Aktiver Nav-Eintrag: zusätzliche Unterstreichung als farb-unabhängiger Indikator */
|
||||
.nav-item[aria-current="page"] {
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------
|
||||
* Windows High Contrast / Forced Colors
|
||||
* -------------------------------------------------------- */
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
background: var(--color-priority-urgent);
|
||||
color: var(--color-text-on-accent);
|
||||
font-size: var(--text-2xs);
|
||||
font-weight: 700;
|
||||
font-weight: var(--font-weight-bold);
|
||||
line-height: 16px;
|
||||
text-align: center;
|
||||
pointer-events: none;
|
||||
@@ -37,7 +37,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
color: var(--color-accent);
|
||||
color: var(--module-reminders);
|
||||
}
|
||||
|
||||
.toast__reminder-text {
|
||||
@@ -57,7 +57,7 @@
|
||||
|
||||
.toast__reminder-text span {
|
||||
font-size: var(--text-sm, 0.875rem);
|
||||
font-weight: 500;
|
||||
font-weight: var(--font-weight-medium);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
@@ -79,7 +79,7 @@
|
||||
|
||||
.reminder-section__title {
|
||||
font-size: var(--text-sm, 0.875rem);
|
||||
font-weight: 600;
|
||||
font-weight: var(--font-weight-semibold);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
|
||||
@@ -172,8 +172,10 @@
|
||||
--module-birthdays: var(--_module-birthdays); /* Rose - Geburtstage */
|
||||
--_module-budget: #0F766E;
|
||||
--module-budget: var(--_module-budget); /* Teal-700 - Finanzen, Stabilität */
|
||||
--_module-settings: #6E7781;
|
||||
--module-settings: var(--_module-settings); /* Grau - Konfiguration */
|
||||
--_module-settings: #6E7781;
|
||||
--module-settings: var(--_module-settings); /* Grau - Konfiguration */
|
||||
--_module-reminders: #0E7490;
|
||||
--module-reminders: var(--_module-reminders); /* Cyan-700 - Erinnerungen, 6.3:1 auf weiß — WCAG AA */
|
||||
|
||||
/* --------------------------------------------------------
|
||||
* 5. Farben - Mahlzeit-Typen
|
||||
@@ -246,6 +248,10 @@
|
||||
--shadow-md: var(--_shadow-md);
|
||||
--_shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.12), 0 2px 6px rgba(0, 0, 0, 0.04);
|
||||
--shadow-lg: var(--_shadow-lg);
|
||||
--_shadow-xl: 0 16px 48px rgba(0, 0, 0, 0.18), 0 4px 12px rgba(0, 0, 0, 0.06);
|
||||
--shadow-xl: var(--_shadow-xl);
|
||||
--_shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.08);
|
||||
--shadow-xs: var(--_shadow-xs);
|
||||
|
||||
/* --------------------------------------------------------
|
||||
* 9. Border-Radien
|
||||
@@ -320,7 +326,7 @@
|
||||
--nav-height-mobile: 56px;
|
||||
--nav-bottom-height: calc(var(--nav-height-mobile) + 12px); /* scroll (56px) + dots-indicator (12px) */
|
||||
--sidebar-width: 56px; /* collapsed icon-only (1024–1279px) */
|
||||
--sidebar-width-expanded: 220px; /* full sidebar (1280px+), max 240px laut Spec */
|
||||
--sidebar-width-expanded: 220px; /* full sidebar (1440px+), max 240px laut Spec */
|
||||
--content-max-width: 1280px;
|
||||
--content-max-width-narrow: 720px;
|
||||
--cal-hour-height: 56px;
|
||||
@@ -403,6 +409,7 @@
|
||||
--glass-tint-strength: var(--_glass-tint-strength);
|
||||
|
||||
/* b) Blur-Stufen */
|
||||
--blur-2xs: blur(2px);
|
||||
--blur-xs: blur(4px);
|
||||
--blur-sm: blur(8px);
|
||||
--blur-md: blur(16px);
|
||||
@@ -523,8 +530,9 @@
|
||||
--_module-notes: #FCD34D;
|
||||
--_module-contacts: #60A5FA;
|
||||
--_module-birthdays: #FB7185;
|
||||
--_module-budget: #2DD4BF;
|
||||
--_module-settings: #94A3B8;
|
||||
--_module-budget: #2DD4BF;
|
||||
--_module-settings: #94A3B8;
|
||||
--_module-reminders: #22D3EE; /* Cyan-400 */
|
||||
|
||||
--_meal-breakfast: #F59E0B;
|
||||
--_meal-breakfast-light: #332400;
|
||||
@@ -545,6 +553,8 @@
|
||||
--_shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.25);
|
||||
--_shadow-md: 0 4px 12px rgba(0, 0, 0, 0.35);
|
||||
--_shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.45);
|
||||
--_shadow-xl: 0 16px 48px rgba(0, 0, 0, 0.60), 0 4px 12px rgba(0, 0, 0, 0.25);
|
||||
--_shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.30);
|
||||
|
||||
--_glass-bg: rgba(28, 28, 26, 0.75);
|
||||
--_glass-bg-hover: rgba(38, 38, 36, 0.82);
|
||||
@@ -625,8 +635,9 @@
|
||||
--_module-notes: #FCD34D;
|
||||
--_module-contacts: #60A5FA;
|
||||
--_module-birthdays: #FB7185;
|
||||
--_module-budget: #2DD4BF; /* Teal-400 */
|
||||
--_module-settings: #94A3B8;
|
||||
--_module-budget: #2DD4BF; /* Teal-400 */
|
||||
--_module-settings: #94A3B8;
|
||||
--_module-reminders: #22D3EE; /* Cyan-400 */
|
||||
|
||||
/* Mahlzeit-Typ */
|
||||
--_meal-breakfast: #F59E0B;
|
||||
@@ -651,6 +662,8 @@
|
||||
--_shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.25);
|
||||
--_shadow-md: 0 4px 12px rgba(0, 0, 0, 0.35);
|
||||
--_shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.45);
|
||||
--_shadow-xl: 0 16px 48px rgba(0, 0, 0, 0.60), 0 4px 12px rgba(0, 0, 0, 0.25);
|
||||
--_shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.30);
|
||||
|
||||
/* Glass */
|
||||
--_glass-bg: rgba(28, 28, 26, 0.75);
|
||||
@@ -688,6 +701,7 @@
|
||||
--glass-highlight: transparent;
|
||||
--glass-highlight-subtle: transparent;
|
||||
--glass-tint-strength: 0%;
|
||||
--blur-2xs: blur(0px);
|
||||
--blur-xs: blur(0px);
|
||||
--blur-sm: blur(0px);
|
||||
--blur-md: blur(0px);
|
||||
@@ -712,6 +726,7 @@
|
||||
--glass-border-subtle: var(--color-text-secondary);
|
||||
--glass-highlight: transparent;
|
||||
--glass-highlight-subtle: transparent;
|
||||
--blur-2xs: blur(0px);
|
||||
--blur-xs: blur(0px);
|
||||
--blur-sm: blur(0px);
|
||||
--blur-md: blur(0px);
|
||||
|
||||
+2
-5
@@ -58,10 +58,10 @@ app.use(helmet({
|
||||
// Alpine.js CDN (optional, falls verwendet)
|
||||
'https://cdn.jsdelivr.net',
|
||||
],
|
||||
styleSrc: ["'self'", "'unsafe-inline'", 'https://cdn.jsdelivr.net'],
|
||||
styleSrc: ["'self'", "'unsafe-inline'"],
|
||||
imgSrc: ["'self'", 'data:'],
|
||||
connectSrc: ["'self'"],
|
||||
fontSrc: ["'self'", 'data:', 'https://cdn.jsdelivr.net'],
|
||||
fontSrc: ["'self'"],
|
||||
objectSrc: ["'none'"],
|
||||
frameSrc: ["'none'"],
|
||||
// upgrade-insecure-requests nur mit HTTPS aktivieren
|
||||
@@ -177,9 +177,6 @@ function sendOpenApi(req, res) {
|
||||
|
||||
app.get('/api/v1/openapi.json', sendOpenApi);
|
||||
app.get('/openapi.json', sendOpenApi);
|
||||
app.get('/docs', (_req, res) => {
|
||||
res.sendFile(path.join(import.meta.dirname, '..', 'public', 'doc-assets', 'swagger.html'));
|
||||
});
|
||||
|
||||
// Alle weiteren API-Routen erfordern Authentifizierung + CSRF-Schutz
|
||||
app.use('/api/v1', requireAuth);
|
||||
|
||||
Reference in New Issue
Block a user