theming: migrate cospend to semantic CSS variables, extract SaveFab, refactor measure page

Replace hardcoded Nord colors with semantic CSS variables across all cospend
pages and shared components (FormSection, ImageUpload, SplitMethodSelector,
UsersList, PaymentModal, BarChart). Remove all dark mode override blocks.
Make BarChart font colors theme-reactive via isDark() + MutationObserver.

Extract reusable SaveFab component and use it on recipe edit and all cospend
edit/add pages. Remove Cancel buttons and back links in favor of browser
navigation. Replace raw checkboxes with Toggle component.

Move fitness measurement add/edit forms to separate routes with SaveFab.
Collapse profile section (sex/height) by default on the measure page.

Document theming rules in CLAUDE.md for future reference.
This commit is contained in:
2026-04-02 20:38:32 +02:00
parent 08a26ff4ac
commit 07610a498f
21 changed files with 927 additions and 2568 deletions
+47
View File
@@ -29,3 +29,50 @@ Svelte 5 removed event modifiers like `on:click|preventDefault`. Use inline hand
Generates a Svelte Playground link with the provided code.
After completing the code, ask the user if they want a playground link. Only call this tool after user confirmation and NEVER if code was written to files in their project.
## Theming Rules
### Semantic CSS Variables (ALWAYS use these, NEVER hardcode Nord values for themed properties)
| Purpose | Variable | Light resolves to | Dark resolves to |
|---|---|---|---|
| Page background | `--color-bg-primary` | white/light | dark |
| Card/section bg | `--color-surface` | nord6-ish | nord1-ish |
| Secondary bg | `--color-bg-secondary` | slightly darker | slightly lighter |
| Tertiary bg (inputs, insets) | `--color-bg-tertiary` | nord5-ish | nord2-ish |
| Hover/elevated bg | `--color-bg-elevated` | nord4-ish | nord3-ish |
| Primary text | `--color-text-primary` | dark text | light text |
| Secondary text (labels, muted) | `--color-text-secondary` | nord3 | nord4 |
| Tertiary text (descriptions) | `--color-text-tertiary` | nord2 | nord5 |
| Borders | `--color-border` | nord4 | nord2/3 |
### What NOT to do
- **NEVER** use `var(--nord0)` through `var(--nord6)` for backgrounds, text, or borders — these don't adapt to theme
- **NEVER** write `@media (prefers-color-scheme: dark)` or `:global(:root[data-theme="dark"])` override blocks — semantic variables handle both themes automatically
- **NEVER** use `var(--font-default-dark)` or `var(--accent-dark)` — these are legacy
### Accent colors (OK to use directly, they work in both themes)
- `var(--blue)`, `var(--red)`, `var(--green)`, `var(--orange)` — named accent colors
- `var(--nord10)`, `var(--nord11)`, `var(--nord12)`, `var(--nord14)` — OK for hover states of accent-colored buttons only
### Chart.js theme reactivity
Charts don't use CSS variables. Use the `isDark()` pattern from `FitnessChart.svelte`:
```js
function isDark() {
const theme = document.documentElement.getAttribute('data-theme');
if (theme === 'dark') return true;
if (theme === 'light') return false;
return window.matchMedia('(prefers-color-scheme: dark)').matches;
}
const textColor = isDark() ? '#D8DEE9' : '#2E3440';
```
Re-create the chart on theme change via `MutationObserver` on `data-theme` + `matchMedia` listener.
### Form inputs
- Background: `var(--color-bg-tertiary)`
- Border: `var(--color-border)`
- Text: `var(--color-text-primary)`
- Label: `var(--color-text-secondary)`
### Toggle component
Use `Toggle.svelte` (iOS-style) instead of raw `<input type="checkbox">` for user-facing boolean switches.
+4 -22
View File
@@ -13,35 +13,17 @@
<style>
.form-section {
background: var(--nord6);
background: var(--color-surface);
padding: 1.5rem;
border-radius: 0.75rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
border: 1px solid var(--nord4);
border: 1px solid var(--color-border);
}
.form-section h2 {
margin-top: 0;
margin-bottom: 1rem;
color: var(--nord0);
color: var(--color-text-primary);
font-size: 1.25rem;
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .form-section {
background: var(--nord1);
border-color: var(--nord2);
}
:global(:root:not([data-theme="light"])) .form-section h2 {
color: var(--font-default-dark);
}
}
:global(:root[data-theme="dark"]) .form-section {
background: var(--nord1);
border-color: var(--nord2);
}
:global(:root[data-theme="dark"]) .form-section h2 {
color: var(--font-default-dark);
}
</style>
</style>
+18 -86
View File
@@ -61,7 +61,7 @@
<div class="form-section">
<h2>{title}</h2>
{#if currentImage}
<div class="current-image">
<img src={currentImage} alt="Receipt" class="receipt-preview" />
@@ -72,7 +72,7 @@
</div>
</div>
{/if}
{#if imagePreview}
<div class="image-preview">
<img src={imagePreview} alt="Receipt preview" />
@@ -93,17 +93,17 @@
<small>JPEG, PNG, WebP (max 5MB)</small>
</div>
</label>
<input
type="file"
id="image"
accept="image/jpeg,image/jpg,image/png,image/webp"
<input
type="file"
id="image"
accept="image/jpeg,image/jpg,image/png,image/webp"
onchange={handleImageChange}
disabled={uploading}
hidden
/>
</div>
{/if}
{#if uploading}
<div class="upload-status">Uploading image...</div>
{/if}
@@ -111,114 +111,55 @@
<style>
.form-section {
background: var(--nord6);
background: var(--color-surface);
padding: 1.5rem;
border-radius: 0.75rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
border: 1px solid var(--nord4);
border: 1px solid var(--color-border);
}
.form-section h2 {
margin-top: 0;
margin-bottom: 1rem;
color: var(--nord0);
color: var(--color-text-primary);
font-size: 1.25rem;
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .form-section {
background: var(--nord1);
border-color: var(--nord2);
}
:global(:root:not([data-theme="light"])) .form-section h2 {
color: var(--font-default-dark);
}
}
:global(:root[data-theme="dark"]) .form-section {
background: var(--nord1);
border-color: var(--nord2);
}
:global(:root[data-theme="dark"]) .form-section h2 {
color: var(--font-default-dark);
}
.image-upload {
border: 2px dashed var(--nord4);
border: 2px dashed var(--color-border);
border-radius: 0.5rem;
padding: 2rem;
text-align: center;
cursor: pointer;
transition: all 0.2s;
background-color: var(--nord5);
background-color: var(--color-bg-tertiary);
}
.image-upload:hover {
border-color: var(--blue);
background-color: var(--nord4);
background-color: var(--color-bg-elevated);
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .image-upload {
background-color: var(--nord2);
border-color: var(--nord3);
}
:global(:root:not([data-theme="light"])) .image-upload:hover {
background-color: var(--nord3);
}
}
:global(:root[data-theme="dark"]) .image-upload {
background-color: var(--nord2);
border-color: var(--nord3);
}
:global(:root[data-theme="dark"]) .image-upload:hover {
background-color: var(--nord3);
}
.upload-label {
cursor: pointer;
display: block;
}
.upload-content svg {
color: var(--nord3);
color: var(--color-text-secondary);
margin-bottom: 1rem;
}
.upload-content p {
margin: 0 0 0.5rem 0;
font-weight: 500;
color: var(--nord0);
color: var(--color-text-primary);
}
.upload-content small {
color: var(--nord3);
color: var(--color-text-secondary);
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .upload-content svg {
color: var(--nord4);
}
:global(:root:not([data-theme="light"])) .upload-content p {
color: var(--font-default-dark);
}
:global(:root:not([data-theme="light"])) .upload-content small {
color: var(--nord4);
}
}
:global(:root[data-theme="dark"]) .upload-content svg {
color: var(--nord4);
}
:global(:root[data-theme="dark"]) .upload-content p {
color: var(--font-default-dark);
}
:global(:root[data-theme="dark"]) .upload-content small {
color: var(--nord4);
}
.image-preview {
text-align: center;
}
@@ -255,22 +196,13 @@
max-height: 200px;
object-fit: cover;
border-radius: 0.5rem;
border: 1px solid var(--nord4);
border: 1px solid var(--color-border);
margin-bottom: 0.75rem;
display: block;
margin-left: auto;
margin-right: auto;
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .receipt-preview {
border-color: var(--nord2);
}
}
:global(:root[data-theme="dark"]) .receipt-preview {
border-color: var(--nord2);
}
.image-actions {
display: flex;
justify-content: center;
@@ -282,4 +214,4 @@
font-size: 0.9rem;
text-align: center;
}
</style>
</style>
+51
View File
@@ -0,0 +1,51 @@
<script>
import Check from '$lib/assets/icons/Check.svelte';
let { disabled = false, onclick, label = 'Save', type = 'submit' } = $props();
</script>
<button
{type}
class="fab-save"
{onclick}
{disabled}
aria-label={label}
>
<Check fill="white" width="2rem" height="2rem" />
</button>
<style>
.fab-save {
position: fixed;
bottom: 0;
right: 0;
width: 1rem;
height: 1rem;
padding: 2rem;
margin: 2rem;
border: none;
border-radius: var(--radius-pill);
background-color: var(--red);
display: grid;
justify-content: center;
align-content: center;
cursor: pointer;
z-index: 100;
transition: background-color 0.2s;
}
.fab-save:hover, .fab-save:focus {
background-color: var(--nord11);
}
.fab-save:disabled {
opacity: 0.5;
cursor: not-allowed;
}
@media screen and (max-width: 500px) {
.fab-save {
margin: 1rem;
}
}
</style>
+23 -22
View File
@@ -20,6 +20,13 @@
// Register Chart.js components
Chart.register(...registerables);
function isDark() {
const theme = document.documentElement.getAttribute('data-theme');
if (theme === 'dark') return true;
if (theme === 'light') return false;
return window.matchMedia('(prefers-color-scheme: dark)').matches;
}
// Nord theme colors for categories
const nordColors = [
'#5E81AC', // Nord Blue
@@ -82,6 +89,12 @@
const ctx = canvas.getContext('2d');
if (!ctx) return;
const dark = isDark();
const textColor = dark ? '#D8DEE9' : '#2E3440';
const tooltipBg = dark ? '#2E3440' : '#ECEFF4';
const tooltipText = dark ? '#ECEFF4' : '#2E3440';
const tooltipBody = dark ? '#D8DEE9' : '#3B4252';
// Convert $state proxy to plain arrays to avoid Chart.js property descriptor issues
const plainLabels = [...(data.labels || [])];
const plainDatasets = (data.datasets || []).map((/** @type {{ label: string, data: number[] }} */ ds) => ({
@@ -123,7 +136,7 @@
display: false
},
ticks: {
color: '#ffffff',
color: textColor,
font: {
family: 'Inter, system-ui, sans-serif',
size: 14,
@@ -157,7 +170,7 @@
labels: {
padding: 20,
usePointStyle: true,
color: '#ffffff',
color: textColor,
font: {
family: 'Inter, system-ui, sans-serif',
size: 14,
@@ -194,7 +207,7 @@
title: {
display: !!title,
text: title,
color: '#ffffff',
color: textColor,
font: {
family: 'Inter, system-ui, sans-serif',
size: 18,
@@ -203,9 +216,9 @@
padding: 20
},
tooltip: {
backgroundColor: '#2e3440',
titleColor: '#ffffff',
bodyColor: '#ffffff',
backgroundColor: tooltipBg,
titleColor: tooltipText,
bodyColor: tooltipBody,
borderWidth: 0,
cornerRadius: 12,
padding: 12,
@@ -275,7 +288,7 @@
ctx.save();
ctx.font = 'bold 14px Inter, system-ui, sans-serif';
ctx.fillStyle = '#ffffff';
ctx.fillStyle = isDark() ? '#D8DEE9' : '#2E3440';
ctx.textAlign = 'center';
ctx.textBaseline = 'bottom';
@@ -367,24 +380,12 @@
<style>
.chart-container {
background: var(--nord6);
border-radius: 0.75rem;
background: var(--color-surface);
border-radius: 12px;
padding: 1.5rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
border: 1px solid var(--nord4);
border: 1px solid var(--color-border);
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .chart-container {
background: var(--nord1);
border-color: var(--nord2);
}
}
:global(:root[data-theme="dark"]) .chart-container {
background: var(--nord1);
border-color: var(--nord2);
}
@media (max-width: 600px) {
.chart-container {
padding: 0.75rem;
+23 -201
View File
@@ -255,7 +255,7 @@
display: flex;
flex-direction: column;
height: 100%;
background: var(--nord6);
background: var(--color-bg-secondary);
}
.panel-header {
@@ -263,14 +263,13 @@
justify-content: space-between;
align-items: center;
padding: 1.5rem;
border-bottom: 1px solid var(--nord4);
background: var(--nord5);
background: var(--color-bg-tertiary);
flex-shrink: 0;
}
.panel-header h2 {
margin: 0;
color: var(--nord0);
color: var(--color-text-primary);
font-size: 1.25rem;
}
@@ -280,13 +279,13 @@
cursor: pointer;
padding: 0.5rem;
border-radius: 0.25rem;
color: var(--nord3);
color: var(--color-text-secondary);
transition: all 0.2s;
}
.close-button:hover {
background: var(--nord4);
color: var(--nord0);
background: var(--color-bg-elevated);
color: var(--color-text-primary);
}
.panel-body {
@@ -303,7 +302,7 @@
.error {
color: var(--red);
background-color: var(--nord6);
background-color: var(--color-bg-secondary);
border-radius: 0.5rem;
border: 1px solid var(--red);
}
@@ -318,8 +317,7 @@
justify-content: space-between;
align-items: flex-start;
padding: 1.5rem;
background: linear-gradient(135deg, var(--nord5), var(--nord4));
border-bottom: 1px solid var(--nord3);
background: var(--color-bg-tertiary);
}
.title-with-category {
@@ -336,7 +334,7 @@
.title-section h1 {
margin: 0;
color: var(--nord0);
color: var(--color-text-primary);
font-size: 1.5rem;
}
@@ -356,7 +354,7 @@
max-height: 100px;
object-fit: cover;
border-radius: 0.5rem;
border: 1px solid var(--nord4);
border: 1px solid var(--color-border);
}
.payment-info {
@@ -378,43 +376,41 @@
.label {
font-weight: 600;
color: var(--nord3);
color: var(--color-text-secondary);
font-size: 0.85rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.value {
color: var(--nord0);
color: var(--color-text-primary);
font-size: 0.95rem;
}
.description {
border-top: 1px solid var(--nord4);
padding-top: 1.5rem;
}
.description h3 {
margin: 0 0 0.75rem 0;
color: var(--nord0);
color: var(--color-text-primary);
font-size: 1rem;
}
.description p {
margin: 0;
color: var(--nord2);
color: var(--color-text-tertiary);
line-height: 1.5;
font-size: 0.95rem;
}
.splits-section {
border-top: 1px solid var(--nord4);
padding: 1.5rem;
}
.splits-section h3 {
margin: 0 0 1rem 0;
color: var(--nord0);
color: var(--color-text-primary);
font-size: 1rem;
}
@@ -429,14 +425,12 @@
justify-content: space-between;
align-items: center;
padding: 0.75rem;
background: var(--nord5);
background: var(--color-bg-primary);
border-radius: 0.5rem;
border: 1px solid var(--nord4);
}
.split-item.current-user {
background: var(--nord8);
border-color: var(--blue);
background: var(--color-bg-tertiary);
}
.split-user {
@@ -453,7 +447,7 @@
.username {
font-weight: 500;
color: var(--nord0);
color: var(--color-text-primary);
font-size: 0.95rem;
}
@@ -481,8 +475,7 @@
.panel-actions {
padding: 1.5rem;
border-top: 1px solid var(--nord4);
background: var(--nord5);
background: var(--color-bg-tertiary);
display: flex;
gap: 1rem;
justify-content: flex-end;
@@ -495,190 +488,19 @@
font-size: 1rem;
cursor: pointer;
transition: all 0.2s;
border: none;
text-decoration: none;
display: inline-block;
text-align: center;
background-color: var(--nord5);
color: var(--nord0);
border: 1px solid var(--nord4);
background-color: var(--color-bg-primary);
color: var(--color-text-primary);
border: 1px solid var(--color-border);
}
.btn-secondary:hover {
background-color: var(--nord4);
background-color: var(--color-bg-elevated);
transform: translateY(-1px);
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .panel-content {
background: var(--nord1);
}
:global(:root:not([data-theme="light"])) .panel-header {
background: var(--nord2);
border-bottom-color: var(--nord3);
}
:global(:root:not([data-theme="light"])) .panel-header h2 {
color: var(--font-default-dark);
}
:global(:root:not([data-theme="light"])) .close-button {
color: var(--nord4);
}
:global(:root:not([data-theme="light"])) .close-button:hover {
background: var(--nord3);
color: var(--font-default-dark);
}
:global(:root:not([data-theme="light"])) .error {
background-color: var(--accent-dark);
}
:global(:root:not([data-theme="light"])) .payment-header {
background: linear-gradient(135deg, var(--nord2), var(--nord3));
}
:global(:root:not([data-theme="light"])) .title-section h1 {
color: var(--font-default-dark);
}
:global(:root:not([data-theme="light"])) .receipt-image img {
border-color: var(--nord2);
}
:global(:root:not([data-theme="light"])) .label {
color: var(--nord4);
}
:global(:root:not([data-theme="light"])) .value {
color: var(--font-default-dark);
}
:global(:root:not([data-theme="light"])) .description {
border-top-color: var(--nord2);
}
:global(:root:not([data-theme="light"])) .description h3 {
color: var(--font-default-dark);
}
:global(:root:not([data-theme="light"])) .description p {
color: var(--nord5);
}
:global(:root:not([data-theme="light"])) .splits-section {
border-top-color: var(--nord2);
}
:global(:root:not([data-theme="light"])) .splits-section h3 {
color: var(--font-default-dark);
}
:global(:root:not([data-theme="light"])) .split-item {
background: var(--nord2);
border-color: var(--nord3);
}
:global(:root:not([data-theme="light"])) .split-item.current-user {
background: var(--nord3);
border-color: var(--blue);
}
:global(:root:not([data-theme="light"])) .username {
color: var(--font-default-dark);
}
:global(:root:not([data-theme="light"])) .panel-actions {
background: var(--nord2);
border-top-color: var(--nord3);
}
:global(:root:not([data-theme="light"])) .btn-secondary {
background-color: var(--nord2);
color: var(--font-default-dark);
border-color: var(--nord3);
}
:global(:root:not([data-theme="light"])) .btn-secondary:hover {
background-color: var(--nord3);
}
}
:global(:root[data-theme="dark"]) .panel-content {
background: var(--nord1);
}
:global(:root[data-theme="dark"]) .panel-header {
background: var(--nord2);
border-bottom-color: var(--nord3);
}
:global(:root[data-theme="dark"]) .panel-header h2 {
color: var(--font-default-dark);
}
:global(:root[data-theme="dark"]) .close-button {
color: var(--nord4);
}
:global(:root[data-theme="dark"]) .close-button:hover {
background: var(--nord3);
color: var(--font-default-dark);
}
:global(:root[data-theme="dark"]) .error {
background-color: var(--accent-dark);
}
:global(:root[data-theme="dark"]) .payment-header {
background: linear-gradient(135deg, var(--nord2), var(--nord3));
}
:global(:root[data-theme="dark"]) .title-section h1 {
color: var(--font-default-dark);
}
:global(:root[data-theme="dark"]) .receipt-image img {
border-color: var(--nord2);
}
:global(:root[data-theme="dark"]) .label {
color: var(--nord4);
}
:global(:root[data-theme="dark"]) .value {
color: var(--font-default-dark);
}
:global(:root[data-theme="dark"]) .description {
border-top-color: var(--nord2);
}
:global(:root[data-theme="dark"]) .description h3 {
color: var(--font-default-dark);
}
:global(:root[data-theme="dark"]) .description p {
color: var(--nord5);
}
:global(:root[data-theme="dark"]) .splits-section {
border-top-color: var(--nord2);
}
:global(:root[data-theme="dark"]) .splits-section h3 {
color: var(--font-default-dark);
}
:global(:root[data-theme="dark"]) .split-item {
background: var(--nord2);
border-color: var(--nord3);
}
:global(:root[data-theme="dark"]) .split-item.current-user {
background: var(--nord3);
border-color: var(--blue);
}
:global(:root[data-theme="dark"]) .username {
color: var(--font-default-dark);
}
:global(:root[data-theme="dark"]) .panel-actions {
background: var(--nord2);
border-top-color: var(--nord3);
}
:global(:root[data-theme="dark"]) .btn-secondary {
background-color: var(--nord2);
color: var(--font-default-dark);
border-color: var(--nord3);
}
:global(:root[data-theme="dark"]) .btn-secondary:hover {
background-color: var(--nord3);
}
@media (max-width: 768px) {
.panel-content {
height: 100vh;
@@ -216,38 +216,20 @@
<style>
.form-section {
background: var(--nord6);
background: var(--color-surface);
padding: 1.5rem;
border-radius: 0.75rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
border: 1px solid var(--nord4);
border: 1px solid var(--color-border);
}
.form-section h2 {
margin-top: 0;
margin-bottom: 1rem;
color: var(--nord0);
color: var(--color-text-primary);
font-size: 1.25rem;
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .form-section {
background: var(--nord1);
border-color: var(--nord2);
}
:global(:root:not([data-theme="light"])) .form-section h2 {
color: var(--font-default-dark);
}
}
:global(:root[data-theme="dark"]) .form-section {
background: var(--nord1);
border-color: var(--nord2);
}
:global(:root[data-theme="dark"]) .form-section h2 {
color: var(--font-default-dark);
}
.form-group {
margin-bottom: 1rem;
}
@@ -256,27 +238,18 @@
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: var(--nord2);
color: var(--color-text-secondary);
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) label {
color: var(--nord5);
}
}
:global(:root[data-theme="dark"]) label {
color: var(--nord5);
}
select {
width: 100%;
padding: 0.75rem;
border: 1px solid var(--nord4);
border: 1px solid var(--color-border);
border-radius: 0.5rem;
font-size: 1rem;
box-sizing: border-box;
background-color: var(--nord6);
color: var(--nord0);
background-color: var(--color-bg-tertiary);
color: var(--color-text-primary);
}
select:focus {
@@ -285,75 +258,31 @@
box-shadow: 0 0 0 2px rgba(94, 129, 172, 0.2);
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) select {
background-color: var(--nord2);
color: var(--font-default-dark);
border-color: var(--nord3);
}
}
:global(:root[data-theme="dark"]) select {
background-color: var(--nord2);
color: var(--font-default-dark);
border-color: var(--nord3);
}
.proportional-splits, .personal-splits {
margin-top: 1rem;
}
.proportional-splits {
border: 1px solid var(--nord4);
border: 1px solid var(--color-border);
border-radius: 0.5rem;
padding: 1rem;
margin-bottom: 1rem;
background-color: var(--nord5);
background-color: var(--color-bg-tertiary);
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .proportional-splits {
border-color: var(--nord3);
background-color: var(--nord2);
}
}
:global(:root[data-theme="dark"]) .proportional-splits {
border-color: var(--nord3);
background-color: var(--nord2);
}
.proportional-splits h3, .personal-splits h3 {
margin-top: 0;
margin-bottom: 1rem;
color: var(--nord0);
color: var(--color-text-primary);
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .proportional-splits h3,
:global(:root:not([data-theme="light"])) .personal-splits h3 {
color: var(--font-default-dark);
}
}
:global(:root[data-theme="dark"]) .proportional-splits h3,
:global(:root[data-theme="dark"]) .personal-splits h3 {
color: var(--font-default-dark);
}
.personal-splits .description {
color: var(--nord2);
color: var(--color-text-secondary);
font-size: 0.9rem;
margin-bottom: 1rem;
font-style: italic;
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .personal-splits .description {
color: var(--nord4);
}
}
:global(:root[data-theme="dark"]) .personal-splits .description {
color: var(--nord4);
}
.split-input {
display: flex;
align-items: center;
@@ -369,11 +298,11 @@
.split-input input {
max-width: 120px;
padding: 0.75rem;
border: 1px solid var(--nord4);
border: 1px solid var(--color-border);
border-radius: 0.5rem;
font-size: 1rem;
background-color: var(--nord6);
color: var(--nord0);
background-color: var(--color-bg-tertiary);
color: var(--color-text-primary);
box-sizing: border-box;
}
@@ -383,52 +312,19 @@
box-shadow: 0 0 0 2px rgba(94, 129, 172, 0.2);
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .split-input input {
background-color: var(--nord2);
color: var(--font-default-dark);
border-color: var(--nord3);
}
}
:global(:root[data-theme="dark"]) .split-input input {
background-color: var(--nord2);
color: var(--font-default-dark);
border-color: var(--nord3);
}
.remainder-info {
margin-top: 1rem;
padding: 1rem;
background-color: var(--nord5);
background-color: var(--color-bg-tertiary);
border-radius: 0.5rem;
border: 1px solid var(--nord4);
border: 1px solid var(--color-border);
}
.remainder-info.error {
background-color: var(--nord6);
background-color: var(--color-bg-secondary);
border-color: var(--red);
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .remainder-info {
background-color: var(--nord2);
border-color: var(--nord3);
}
:global(:root:not([data-theme="light"])) .remainder-info.error {
background-color: var(--accent-dark);
border-color: var(--red);
}
}
:global(:root[data-theme="dark"]) .remainder-info {
background-color: var(--nord2);
border-color: var(--nord3);
}
:global(:root[data-theme="dark"]) .remainder-info.error {
background-color: var(--accent-dark);
border-color: var(--red);
}
.remainder-info span {
display: block;
margin-bottom: 0.5rem;
@@ -443,38 +339,18 @@
}
.split-preview {
background-color: var(--nord5);
background-color: var(--color-bg-tertiary);
padding: 1rem;
border-radius: 0.5rem;
border: 1px solid var(--nord4);
border: 1px solid var(--color-border);
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .split-preview {
background-color: var(--nord2);
border-color: var(--nord3);
}
}
:global(:root[data-theme="dark"]) .split-preview {
background-color: var(--nord2);
border-color: var(--nord3);
}
.split-preview h3 {
margin-top: 0;
margin-bottom: 1rem;
color: var(--nord0);
color: var(--color-text-primary);
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .split-preview h3 {
color: var(--font-default-dark);
}
}
:global(:root[data-theme="dark"]) .split-preview h3 {
color: var(--font-default-dark);
}
.split-item {
display: flex;
justify-content: space-between;
@@ -489,18 +365,9 @@
}
.username {
color: var(--nord0);
color: var(--color-text-primary);
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .username {
color: var(--font-default-dark);
}
}
:global(:root[data-theme="dark"]) .username {
color: var(--font-default-dark);
}
.amount.positive {
color: var(--green);
font-weight: 500;
@@ -510,4 +377,4 @@
color: var(--red);
font-weight: 500;
}
</style>
</style>
+16 -87
View File
@@ -14,10 +14,10 @@
canRemoveUsers?: boolean,
newUser?: string
}>();
function addUser() {
if (predefinedMode) return;
if (newUser.trim() && !users.includes(newUser.trim())) {
users = [...users, newUser.trim()];
newUser = '';
@@ -36,7 +36,7 @@
<div class="form-section">
<h2>Split Between Users</h2>
{#if predefinedMode}
<div class="predefined-users">
<p class="predefined-note">Splitting between predefined users:</p>
@@ -84,38 +84,20 @@
<style>
.form-section {
background: var(--nord6);
background: var(--color-surface);
padding: 1.5rem;
border-radius: 0.75rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
border: 1px solid var(--nord4);
border: 1px solid var(--color-border);
}
.form-section h2 {
margin-top: 0;
margin-bottom: 1rem;
color: var(--nord0);
color: var(--color-text-primary);
font-size: 1.25rem;
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .form-section {
background: var(--nord1);
border-color: var(--nord2);
}
:global(:root:not([data-theme="light"])) .form-section h2 {
color: var(--font-default-dark);
}
}
:global(:root[data-theme="dark"]) .form-section {
background: var(--nord1);
border-color: var(--nord2);
}
:global(:root[data-theme="dark"]) .form-section h2 {
color: var(--font-default-dark);
}
.users-list {
display: flex;
flex-wrap: wrap;
@@ -127,41 +109,21 @@
display: flex;
align-items: center;
gap: 0.5rem;
background-color: var(--nord5);
background-color: var(--color-bg-tertiary);
padding: 0.5rem 0.75rem;
border-radius: 1rem;
border: 1px solid var(--nord4);
border: 1px solid var(--color-border);
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .user-item {
background-color: var(--nord2);
border-color: var(--nord3);
}
}
:global(:root[data-theme="dark"]) .user-item {
background-color: var(--nord2);
border-color: var(--nord3);
}
.user-item.with-profile {
gap: 0.75rem;
}
.user-item .username {
font-weight: 500;
color: var(--nord0);
color: var(--color-text-primary);
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .user-item .username {
color: var(--font-default-dark);
}
}
:global(:root[data-theme="dark"]) .user-item .username {
color: var(--font-default-dark);
}
.you-badge {
background-color: var(--blue);
color: white;
@@ -172,39 +134,19 @@
}
.predefined-users {
background-color: var(--nord5);
background-color: var(--color-bg-tertiary);
padding: 1rem;
border-radius: 0.5rem;
border: 1px solid var(--nord4);
border: 1px solid var(--color-border);
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .predefined-users {
background-color: var(--nord2);
border-color: var(--nord3);
}
}
:global(:root[data-theme="dark"]) .predefined-users {
background-color: var(--nord2);
border-color: var(--nord3);
}
.predefined-note {
margin: 0 0 1rem 0;
color: var(--nord2);
color: var(--color-text-secondary);
font-size: 0.9rem;
font-style: italic;
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .predefined-note {
color: var(--nord4);
}
}
:global(:root[data-theme="dark"]) .predefined-note {
color: var(--nord4);
}
.remove-user {
background-color: var(--red);
color: white;
@@ -229,11 +171,11 @@
.add-user input {
flex: 1;
padding: 0.75rem;
border: 1px solid var(--nord4);
border: 1px solid var(--color-border);
border-radius: 0.5rem;
font-size: 1rem;
background-color: var(--nord6);
color: var(--nord0);
background-color: var(--color-bg-tertiary);
color: var(--color-text-primary);
box-sizing: border-box;
}
@@ -243,19 +185,6 @@
box-shadow: 0 0 0 2px rgba(94, 129, 172, 0.2);
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .add-user input {
background-color: var(--nord2);
color: var(--font-default-dark);
border-color: var(--nord3);
}
}
:global(:root[data-theme="dark"]) .add-user input {
background-color: var(--nord2);
color: var(--font-default-dark);
border-color: var(--nord3);
}
.add-user button {
background-color: var(--blue);
color: white;
@@ -270,4 +199,4 @@
background-color: var(--nord10);
transform: translateY(-1px);
}
</style>
</style>
@@ -2,7 +2,7 @@
import { enhance } from '$app/forms';
import { tick } from 'svelte';
import type { ActionData, PageData } from './$types';
import Check from '$lib/assets/icons/Check.svelte';
import SaveFab from '$lib/components/SaveFab.svelte';
import Cross from '$lib/assets/icons/Cross.svelte';
import SeasonSelect from '$lib/components/recipes/SeasonSelect.svelte';
import TranslationApproval from '$lib/components/recipes/TranslationApproval.svelte';
@@ -479,34 +479,6 @@
font-size: 1.3rem;
color: white;
}
.fab-save {
position: fixed;
bottom: 0;
right: 0;
width: 1rem;
height: 1rem;
padding: 2rem;
margin: 2rem;
border-radius: var(--radius-pill);
background-color: var(--red);
display: grid;
justify-content: center;
align-content: center;
z-index: 100;
animation: unset !important;
}
.fab-save:hover, .fab-save:focus {
background-color: var(--nord0) !important;
}
.fab-save:disabled {
opacity: 0.5;
cursor: not-allowed;
}
@media screen and (max-width: 500px) {
.fab-save {
margin: 1rem;
}
}
.submit_buttons {
display: flex;
margin-inline: auto;
@@ -1147,13 +1119,4 @@
</div>
{/if}
<!-- FAB save button -->
<button
type="button"
class="fab-save action_button"
onclick={saveRecipe}
disabled={submitting}
aria-label="Rezept speichern"
>
<Check fill="white" width="2rem" height="2rem" />
</button>
<SaveFab type="button" onclick={saveRecipe} disabled={submitting} label="Rezept speichern" />
+10 -29
View File
@@ -108,12 +108,13 @@
.side-panel {
position: fixed;
top: 4rem;
top: 0;
right: 0;
width: 400px;
height: calc(100vh - 4rem);
background: #fbf9f3;
border-left: 1px solid #dee2e6;
height: 100vh;
padding-top: var(--header-h, 3rem);
background: var(--color-bg-tertiary);
border-left: 1px solid var(--color-border);
box-shadow: -2px 0 10px rgba(0, 0, 0, 0.1);
z-index: 100;
overflow-y: auto;
@@ -139,17 +140,6 @@
overflow-y: auto;
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .side-panel {
background: var(--background-dark);
border-left-color: #434C5E;
}
}
:global(:root[data-theme="dark"]) .side-panel {
background: var(--background-dark);
border-left-color: #434C5E;
}
@media (max-width: 768px) {
.layout-container.has-modal .main-content {
margin-right: 0;
@@ -157,11 +147,12 @@
.side-panel {
position: fixed;
top: 4rem;
top: 0;
left: 0;
right: 0;
width: 100%;
height: calc(100vh - 4rem);
height: 100vh;
padding-top: var(--header-h, 3rem);
transform: translateY(100%);
}
@@ -188,8 +179,9 @@
min-width: unset;
max-width: unset;
width: 100%;
padding-top: 0;
border-left: none;
border-top: 1px solid #dee2e6;
border-top: 1px solid var(--color-border);
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);
top: auto;
bottom: 0;
@@ -200,15 +192,4 @@
transform: translateY(0);
}
}
@media (max-width: 768px) and (prefers-color-scheme: dark) {
.side-panel {
border-top-color: #434C5E;
}
}
@media (max-width: 768px) {
:global(:root[data-theme="dark"]) .side-panel {
border-top-color: #434C5E;
}
}
</style>
+26 -214
View File
@@ -310,7 +310,7 @@
text-align: center;
font-size: 2.5rem;
margin-block: 0.5rem 1.5rem;
color: var(--nord0);
color: var(--color-text-primary);
}
.loading, .error {
@@ -321,26 +321,10 @@
.error {
color: var(--red);
background-color: var(--nord6);
background-color: var(--color-bg-secondary);
border-radius: 0.5rem;
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) h1 {
color: var(--font-default-dark);
}
:global(:root:not([data-theme="light"])) .error {
background-color: var(--accent-dark);
}
}
:global(:root[data-theme="dark"]) h1 {
color: var(--font-default-dark);
}
:global(:root[data-theme="dark"]) .error {
background-color: var(--accent-dark);
}
.positive {
color: var(--green);
@@ -379,11 +363,7 @@
}
.recent-activity {
background: var(--nord6);
padding: 1.5rem;
border-radius: 0.75rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
border: 1px solid var(--nord4);
padding: 1.5rem 0;
max-width: 800px;
margin-left: auto;
margin-right: auto;
@@ -399,21 +379,21 @@
.recent-activity h2 {
margin: 0;
color: var(--nord0);
color: var(--color-text-primary);
}
.filter-label {
font-weight: 400;
font-size: 1rem;
color: var(--nord3);
color: var(--color-text-secondary);
}
.clear-filter {
background: none;
border: 1px solid var(--nord4);
border: 1px solid var(--color-border);
border-radius: 0.5rem;
padding: 0.25rem 0.75rem;
color: var(--nord3);
color: var(--color-text-secondary);
cursor: pointer;
font-size: 0.85rem;
white-space: nowrap;
@@ -421,67 +401,17 @@
}
.clear-filter:hover {
border-color: var(--blue);
color: var(--blue);
border-color: var(--color-primary);
color: var(--color-primary);
}
.no-results {
text-align: center;
color: var(--nord3);
color: var(--color-text-secondary);
font-style: italic;
padding: 1rem 0;
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .recent-activity {
background: var(--accent-dark);
border-color: var(--nord2);
}
:global(:root:not([data-theme="light"])) .recent-activity h2 {
color: var(--font-default-dark);
}
:global(:root:not([data-theme="light"])) .filter-label {
color: var(--nord4);
}
:global(:root:not([data-theme="light"])) .clear-filter {
border-color: var(--nord3);
color: var(--nord4);
}
:global(:root:not([data-theme="light"])) .clear-filter:hover {
border-color: var(--blue);
color: var(--blue);
}
:global(:root:not([data-theme="light"])) .no-results {
color: var(--nord4);
}
}
:global(:root[data-theme="dark"]) .recent-activity {
background: var(--accent-dark);
border-color: var(--nord2);
}
:global(:root[data-theme="dark"]) .recent-activity h2 {
color: var(--font-default-dark);
}
:global(:root[data-theme="dark"]) .filter-label {
color: var(--nord4);
}
:global(:root[data-theme="dark"]) .clear-filter {
border-color: var(--nord3);
color: var(--nord4);
}
:global(:root[data-theme="dark"]) .clear-filter:hover {
border-color: var(--blue);
color: var(--blue);
}
:global(:root[data-theme="dark"]) .no-results {
color: var(--nord4);
}
.activity-dialog {
display: flex;
flex-direction: column;
@@ -511,11 +441,11 @@
}
.activity-bubble {
background: var(--nord5);
background: var(--color-bg-secondary);
border-radius: 1rem;
padding: 1rem;
position: relative;
border: 1px solid var(--nord4);
border: none;
text-decoration: none;
color: inherit;
display: block;
@@ -524,30 +454,9 @@
}
.activity-message.is-me .activity-bubble {
background: var(--nord8);
border-color: var(--blue);
background: var(--color-bg-tertiary);
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .activity-bubble {
background: var(--nord1);
border-color: var(--nord2);
}
:global(:root:not([data-theme="light"])) .activity-message.is-me .activity-bubble {
background: var(--nord2);
border-color: var(--blue);
}
}
:global(:root[data-theme="dark"]) .activity-bubble {
background: var(--nord1);
border-color: var(--nord2);
}
:global(:root[data-theme="dark"]) .activity-message.is-me .activity-bubble {
background: var(--nord2);
border-color: var(--blue);
}
.activity-bubble:hover {
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
@@ -564,39 +473,23 @@
.activity-bubble::before {
left: -15px;
border-right-color: var(--nord5);
border-right-color: var(--color-bg-secondary);
}
.activity-message.is-me .activity-bubble::before {
left: auto;
right: -15px;
border-left-color: var(--nord8);
border-left-color: var(--color-bg-tertiary);
border-right-color: transparent;
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .activity-bubble::before {
border-right-color: var(--nord1);
}
:global(:root:not([data-theme="light"])) .activity-message.is-me .activity-bubble::before {
border-left-color: var(--nord2);
}
}
:global(:root[data-theme="dark"]) .activity-bubble::before {
border-right-color: var(--nord1);
}
:global(:root[data-theme="dark"]) .activity-message.is-me .activity-bubble::before {
border-left-color: var(--nord2);
}
/* New Settlement Flow Activity Styles */
/* Settlement Flow Activity Styles */
.settlement-flow-activity {
display: block;
text-decoration: none;
color: inherit;
background: linear-gradient(135deg, var(--nord6), var(--nord5));
background: var(--color-bg-secondary);
border: 2px solid var(--green);
border-radius: 1rem;
padding: 1.5rem;
@@ -610,24 +503,6 @@
transform: translateY(-2px);
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .settlement-flow-activity {
background: linear-gradient(135deg, var(--nord2), var(--nord1));
border-color: var(--green);
}
:global(:root:not([data-theme="light"])) .settlement-flow-activity:hover {
box-shadow: 0 6px 20px rgba(163, 190, 140, 0.2);
}
}
:global(:root[data-theme="dark"]) .settlement-flow-activity {
background: linear-gradient(135deg, var(--nord2), var(--nord1));
border-color: var(--green);
}
:global(:root[data-theme="dark"]) .settlement-flow-activity:hover {
box-shadow: 0 6px 20px rgba(163, 190, 140, 0.2);
}
.settlement-activity-content {
width: 100%;
}
@@ -677,19 +552,10 @@
.settlement-date {
font-size: 0.9rem;
color: var(--nord3);
color: var(--color-text-secondary);
text-align: center;
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .settlement-date {
color: var(--nord4);
}
}
:global(:root[data-theme="dark"]) .settlement-date {
color: var(--nord4);
}
.activity-header {
display: flex;
justify-content: space-between;
@@ -715,47 +581,24 @@
}
.category-name {
color: var(--nord3);
color: var(--color-text-tertiary);
font-size: 0.8rem;
font-style: italic;
}
.payment-title {
color: var(--nord0);
color: var(--color-text-primary);
font-size: 1.1rem;
font-weight: 600;
margin: 0;
}
.username {
color: var(--nord3);
color: var(--color-text-secondary);
font-size: 0.9rem;
font-weight: 500;
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .category-name {
color: var(--nord4);
}
:global(:root:not([data-theme="light"])) .payment-title {
color: var(--font-default-dark);
}
:global(:root:not([data-theme="light"])) .username {
color: var(--nord4);
}
}
:global(:root[data-theme="dark"]) .category-name {
color: var(--nord4);
}
:global(:root[data-theme="dark"]) .payment-title {
color: var(--font-default-dark);
}
:global(:root[data-theme="dark"]) .username {
color: var(--nord4);
}
.activity-amount {
font-weight: bold;
font-size: 1rem;
@@ -775,34 +618,18 @@
}
.payment-date {
color: var(--nord3);
color: var(--color-text-secondary);
font-size: 0.9rem;
}
.payment-description {
color: var(--nord2);
color: var(--color-text-tertiary);
font-size: 0.9rem;
font-style: italic;
margin-top: 0.25rem;
line-height: 1.3;
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .payment-date {
color: var(--nord4);
}
:global(:root:not([data-theme="light"])) .payment-description {
color: var(--nord5);
}
}
:global(:root[data-theme="dark"]) .payment-date {
color: var(--nord4);
}
:global(:root[data-theme="dark"]) .payment-description {
color: var(--nord5);
}
@media (max-width: 600px) {
.cospend-main {
padding: 0.75rem;
@@ -814,7 +641,7 @@
}
.recent-activity {
padding: 0.75rem;
padding: 0.75rem 0;
}
.actions {
@@ -918,25 +745,10 @@
}
.chart-section .loading {
background: var(--nord6);
background: var(--color-bg-secondary);
border-radius: 0.75rem;
padding: 2rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
border: 1px solid var(--nord4);
text-align: center;
color: var(--nord2);
color: var(--color-text-secondary);
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .chart-section .loading {
background: var(--nord1);
border-color: var(--nord2);
color: var(--nord4);
}
}
:global(:root[data-theme="dark"]) .chart-section .loading {
background: var(--nord1);
border-color: var(--nord2);
color: var(--nord4);
}
</style>
+27 -220
View File
@@ -289,23 +289,13 @@
padding: 1rem;
}
h1 {
margin-block: 0 1rem;
margin-inline: auto;
color: var(--nord0);
color: var(--color-text-primary);
text-align: center;
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) h1 {
color: var(--font-default-dark);
}
}
:global(:root[data-theme="dark"]) h1 {
color: var(--font-default-dark);
}
.btn {
padding: 0.75rem 1.5rem;
border-radius: 0.5rem;
@@ -328,35 +318,15 @@
}
.btn-secondary {
background-color: var(--nord5);
color: var(--nord0);
border: 1px solid var(--nord4);
background-color: var(--color-bg-tertiary);
color: var(--color-text-primary);
border: 1px solid var(--color-border);
}
.btn-secondary:hover {
background-color: var(--nord4);
background-color: var(--color-bg-elevated);
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .btn-secondary {
background-color: var(--nord2);
color: var(--font-default-dark);
border-color: var(--nord3);
}
:global(:root:not([data-theme="light"])) .btn-secondary:hover {
background-color: var(--nord3);
}
}
:global(:root[data-theme="dark"]) .btn-secondary {
background-color: var(--nord2);
color: var(--font-default-dark);
border-color: var(--nord3);
}
:global(:root[data-theme="dark"]) .btn-secondary:hover {
background-color: var(--nord3);
}
.loading, .error {
text-align: center;
padding: 2rem;
@@ -365,63 +335,31 @@
.error {
color: var(--red);
background-color: var(--nord6);
background-color: var(--color-bg-secondary);
border-radius: 0.5rem;
border: 1px solid var(--red);
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .error {
background-color: var(--accent-dark);
}
}
:global(:root[data-theme="dark"]) .error {
background-color: var(--accent-dark);
}
.empty-state {
text-align: center;
padding: 4rem 2rem;
}
.empty-content svg {
color: var(--nord3);
color: var(--color-text-secondary);
margin-bottom: 1rem;
}
.empty-content h2 {
margin: 0 0 0.5rem 0;
color: var(--nord1);
color: var(--color-text-primary);
}
.empty-content p {
margin: 0 0 2rem 0;
color: var(--nord2);
color: var(--color-text-secondary);
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .empty-content svg {
color: var(--nord4);
}
:global(:root:not([data-theme="light"])) .empty-content h2 {
color: var(--nord5);
}
:global(:root:not([data-theme="light"])) .empty-content p {
color: var(--nord4);
}
}
:global(:root[data-theme="dark"]) .empty-content svg {
color: var(--nord4);
}
:global(:root[data-theme="dark"]) .empty-content h2 {
color: var(--nord5);
}
:global(:root[data-theme="dark"]) .empty-content p {
color: var(--nord4);
}
.payments-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
@@ -431,14 +369,14 @@
.payment-card {
display: block;
background: var(--nord6);
background: var(--color-surface);
border-radius: 0.75rem;
padding: 1.5rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: all 0.2s;
text-decoration: none;
color: inherit;
border: 1px solid var(--nord4);
border: 1px solid var(--color-border);
}
.payment-card:hover {
@@ -446,27 +384,8 @@
text-decoration: none;
color: inherit;
transform: translateY(-1px);
border-color: var(--nord3);
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .payment-card {
background: var(--nord1);
border-color: var(--nord2);
}
:global(:root:not([data-theme="light"])) .payment-card:hover {
border-color: var(--nord3);
}
}
:global(:root[data-theme="dark"]) .payment-card {
background: var(--nord1);
border-color: var(--nord2);
}
:global(:root[data-theme="dark"]) .payment-card:hover {
border-color: var(--nord3);
}
/* Settlement Card Styles */
.settlement-card {
background: linear-gradient(135deg, #e8f5e9, #f1f8e9);
@@ -494,17 +413,10 @@
:global(:root:not([data-theme="light"])) .settlement-card {
background: linear-gradient(135deg, #1a2e1a, #1e2b1e);
}
:global(:root:not([data-theme="light"])) .settlement-card:hover {
box-shadow: 0 6px 20px rgba(163, 190, 140, 0.3);
}
}
:global(:root[data-theme="dark"]) .settlement-card {
background: linear-gradient(135deg, #1a2e1a, #1e2b1e);
}
:global(:root[data-theme="dark"]) .settlement-card:hover {
box-shadow: 0 6px 20px rgba(163, 190, 140, 0.3);
}
:global(:root[data-theme="dark"]) .settlement-card {
background: linear-gradient(135deg, #1a2e1a, #1e2b1e);
}
.settlement-header {
display: flex;
@@ -531,20 +443,11 @@
}
.settlement-date {
color: var(--nord3);
color: var(--color-text-secondary);
font-size: 0.9rem;
font-weight: 500;
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .settlement-date {
color: var(--nord4);
}
}
:global(:root[data-theme="dark"]) .settlement-date {
color: var(--nord4);
}
.settlement-flow {
display: flex;
align-items: center;
@@ -594,25 +497,14 @@
}
.settlement-description {
color: var(--nord2);
color: var(--color-text-tertiary);
margin-top: 1rem;
padding-top: 1rem;
border-top: 1px solid var(--nord4);
border-top: 1px solid var(--color-border);
font-style: italic;
font-size: 0.9rem;
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .settlement-description {
color: var(--nord5);
border-top-color: var(--nord3);
}
}
:global(:root[data-theme="dark"]) .settlement-description {
color: var(--nord5);
border-top-color: var(--nord3);
}
.payment-header {
display: flex;
justify-content: space-between;
@@ -641,29 +533,20 @@
.payment-title h3 {
margin: 0;
color: var(--nord0);
color: var(--color-text-primary);
font-size: 1.25rem;
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .payment-title h3 {
color: var(--font-default-dark);
}
}
:global(:root[data-theme="dark"]) .payment-title h3 {
color: var(--font-default-dark);
}
.payment-meta {
display: flex;
gap: 1rem;
font-size: 0.9rem;
color: var(--nord3);
color: var(--color-text-secondary);
flex-wrap: wrap;
}
.payment-meta .category-name {
color: var(--nord3);
color: var(--color-text-secondary);
font-style: italic;
font-size: 0.8rem;
}
@@ -673,54 +556,20 @@
color: var(--blue);
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .payment-meta {
color: var(--nord4);
}
:global(:root:not([data-theme="light"])) .payment-meta .category-name {
color: var(--nord4);
}
}
:global(:root[data-theme="dark"]) .payment-meta {
color: var(--nord4);
}
:global(:root[data-theme="dark"]) .payment-meta .category-name {
color: var(--nord4);
}
.receipt-thumb {
width: 60px;
height: 60px;
object-fit: cover;
border-radius: 0.5rem;
border: 1px solid var(--nord4);
border: 1px solid var(--color-border);
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .receipt-thumb {
border-color: var(--nord2);
}
}
:global(:root[data-theme="dark"]) .receipt-thumb {
border-color: var(--nord2);
}
.payment-description {
color: var(--nord2);
color: var(--color-text-tertiary);
margin-bottom: 1rem;
font-style: italic;
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .payment-description {
color: var(--nord5);
}
}
:global(:root[data-theme="dark"]) .payment-description {
color: var(--nord5);
}
.payment-details {
margin-bottom: 1rem;
}
@@ -732,58 +581,26 @@
}
.detail-row .label {
color: var(--nord3);
color: var(--color-text-secondary);
font-weight: 500;
}
.detail-row .value {
color: var(--nord0);
color: var(--color-text-primary);
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .detail-row .label {
color: var(--nord4);
}
:global(:root:not([data-theme="light"])) .detail-row .value {
color: var(--font-default-dark);
}
}
:global(:root[data-theme="dark"]) .detail-row .label {
color: var(--nord4);
}
:global(:root[data-theme="dark"]) .detail-row .value {
color: var(--font-default-dark);
}
.splits-summary {
border-top: 1px solid var(--nord4);
border-top: 1px solid var(--color-border);
padding-top: 1rem;
margin-bottom: 1rem;
}
.splits-summary h4 {
margin: 0 0 0.75rem 0;
color: var(--nord0);
color: var(--color-text-primary);
font-size: 1rem;
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .splits-summary {
border-top-color: var(--nord2);
}
:global(:root:not([data-theme="light"])) .splits-summary h4 {
color: var(--font-default-dark);
}
}
:global(:root[data-theme="dark"]) .splits-summary {
border-top-color: var(--nord2);
}
:global(:root[data-theme="dark"]) .splits-summary h4 {
color: var(--font-default-dark);
}
.splits-list {
display: flex;
flex-direction: column;
@@ -797,7 +614,7 @@
}
.split-user {
color: var(--nord2);
color: var(--color-text-tertiary);
}
.split-amount.positive {
@@ -810,16 +627,6 @@
font-weight: 500;
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .split-user {
color: var(--nord5);
}
}
:global(:root[data-theme="dark"]) .split-user {
color: var(--nord5);
}
.pagination {
display: flex;
justify-content: center;
+31 -264
View File
@@ -9,6 +9,8 @@
import SplitMethodSelector from '$lib/components/cospend/SplitMethodSelector.svelte';
import UsersList from '$lib/components/cospend/UsersList.svelte';
import ImageUpload from '$lib/components/ImageUpload.svelte';
import SaveFab from '$lib/components/SaveFab.svelte';
import Toggle from '$lib/components/Toggle.svelte';
let { data, form } = $props();
@@ -465,13 +467,9 @@
<div class="form-group">
<label class="checkbox-label">
<input
type="checkbox"
name="isRecurring"
bind:checked={formData.isRecurring}
value="true"
/>
Make this a recurring payment
<Toggle bind:checked={formData.isRecurring} />
<span>Make this a recurring payment</span>
<input type="hidden" name="isRecurring" value={formData.isRecurring ? 'true' : 'false'} />
</label>
</div>
</div>
@@ -612,14 +610,7 @@
<div class="error">{error}</div>
{/if}
<div class="form-actions">
<a href="/cospend" class="btn-secondary">
Cancel
</a>
<button type="submit" class="btn-primary" disabled={loading}>
{loading ? 'Creating...' : (formData.isRecurring ? 'Create Recurring Payment' : 'Create Payment')}
</button>
</div>
<SaveFab disabled={loading} label="Create payment" />
</form>
</main>
@@ -637,32 +628,16 @@
.header h1 {
margin: 0 0 0.5rem 0;
color: var(--nord0);
color: var(--color-text-primary);
font-size: 2rem;
}
.header p {
margin: 0;
color: var(--nord3);
color: var(--color-text-secondary);
font-size: 1.1rem;
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .header h1 {
color: var(--font-default-dark);
}
:global(:root:not([data-theme="light"])) .header p {
color: var(--nord4);
}
}
:global(:root[data-theme="dark"]) .header h1 {
color: var(--font-default-dark);
}
:global(:root[data-theme="dark"]) .header p {
color: var(--nord4);
}
.payment-form {
display: flex;
flex-direction: column;
@@ -670,38 +645,20 @@
}
.form-section {
background: var(--nord6);
background: var(--color-surface);
padding: 1.5rem;
border-radius: 0.75rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
border: 1px solid var(--nord4);
border: 1px solid var(--color-border);
}
.form-section h2 {
margin-top: 0;
margin-bottom: 1rem;
color: var(--nord0);
color: var(--color-text-primary);
font-size: 1.25rem;
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .form-section {
background: var(--nord1);
border-color: var(--nord2);
}
:global(:root:not([data-theme="light"])) .form-section h2 {
color: var(--font-default-dark);
}
}
:global(:root[data-theme="dark"]) .form-section {
background: var(--nord1);
border-color: var(--nord2);
}
:global(:root[data-theme="dark"]) .form-section h2 {
color: var(--font-default-dark);
}
.form-group {
margin-bottom: 1rem;
}
@@ -716,27 +673,18 @@
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: var(--nord2);
color: var(--color-text-secondary);
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) label {
color: var(--nord5);
}
}
:global(:root[data-theme="dark"]) label {
color: var(--nord5);
}
input, textarea, select {
width: 100%;
padding: 0.75rem;
border: 1px solid var(--nord4);
border: 1px solid var(--color-border);
border-radius: 0.5rem;
font-size: 1rem;
box-sizing: border-box;
background-color: var(--nord6);
color: var(--nord0);
background-color: var(--color-bg-tertiary);
color: var(--color-text-primary);
}
input:focus, textarea:focus, select:focus {
@@ -745,29 +693,12 @@
box-shadow: 0 0 0 2px rgba(94, 129, 172, 0.2);
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) input,
:global(:root:not([data-theme="light"])) textarea,
:global(:root:not([data-theme="light"])) select {
background-color: var(--nord2);
color: var(--font-default-dark);
border-color: var(--nord3);
}
}
:global(:root[data-theme="dark"]) input,
:global(:root[data-theme="dark"]) textarea,
:global(:root[data-theme="dark"]) select {
background-color: var(--nord2);
color: var(--font-default-dark);
border-color: var(--nord3);
}
.error {
background-color: var(--nord6);
background-color: var(--color-bg-secondary);
color: var(--red);
padding: 1rem;
border-radius: 0.5rem;
@@ -775,81 +706,6 @@
border: 1px solid var(--red);
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .error {
background-color: var(--accent-dark);
}
}
:global(:root[data-theme="dark"]) .error {
background-color: var(--accent-dark);
}
.form-actions {
display: flex;
gap: 1rem;
justify-content: flex-end;
}
.btn-primary, .btn-secondary {
padding: 0.75rem 1.5rem;
border-radius: 0.5rem;
font-size: 1rem;
cursor: pointer;
transition: all 0.2s;
}
.btn-primary {
background-color: var(--blue);
color: white;
border: none;
}
.btn-primary:hover:not(:disabled) {
background-color: var(--nord10);
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}
.btn-primary:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-secondary {
background-color: var(--nord5);
color: var(--nord0);
border: 1px solid var(--nord4);
text-decoration: none;
display: inline-block;
text-align: center;
}
.btn-secondary:hover {
background-color: var(--nord4);
text-decoration: none;
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .btn-secondary {
background-color: var(--nord2);
color: var(--font-default-dark);
border-color: var(--nord3);
}
:global(:root:not([data-theme="light"])) .btn-secondary:hover {
background-color: var(--nord3);
}
}
:global(:root[data-theme="dark"]) .btn-secondary {
background-color: var(--nord2);
color: var(--font-default-dark);
border-color: var(--nord3);
}
:global(:root[data-theme="dark"]) .btn-secondary:hover {
background-color: var(--nord3);
}
/* Progressive enhancement styles */
.no-js-only {
display: block;
@@ -862,7 +718,7 @@
.manual-users textarea {
width: 100%;
padding: 0.75rem;
border: 1px solid #ddd;
border: 1px solid var(--color-border);
border-radius: 0.5rem;
font-family: inherit;
font-size: 0.9rem;
@@ -872,91 +728,46 @@
.manual-users p {
margin: 0 0 0.5rem 0;
font-size: 0.9rem;
color: var(--nord2);
color: var(--color-text-secondary);
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .manual-users p {
color: var(--nord4);
}
}
:global(:root[data-theme="dark"]) .manual-users p {
color: var(--nord4);
}
/* Recurring payment styles */
.checkbox-label {
display: flex !important;
align-items: center;
gap: 0.5rem;
gap: 0.75rem;
cursor: pointer;
}
.checkbox-label input[type="checkbox"] {
width: auto;
margin: 0;
}
.recurring-options {
margin-top: 1rem;
padding: 1rem;
background-color: var(--nord5);
background-color: var(--color-bg-tertiary);
border-radius: 0.5rem;
border: 1px solid var(--nord4);
border: 1px solid var(--color-border);
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .recurring-options {
background-color: var(--nord2);
border-color: var(--nord3);
}
}
:global(:root[data-theme="dark"]) .recurring-options {
background-color: var(--nord2);
border-color: var(--nord3);
}
.help-text {
display: block;
margin-top: 0.25rem;
font-size: 0.8rem;
color: var(--nord3);
color: var(--color-text-secondary);
font-style: italic;
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .help-text {
color: var(--nord4);
}
}
:global(:root[data-theme="dark"]) .help-text {
color: var(--nord4);
}
.help-text p {
margin: 0.5rem 0 0.25rem 0;
}
.help-text code {
background-color: var(--nord5);
background-color: var(--color-bg-tertiary);
padding: 0.125rem 0.25rem;
border-radius: 0.25rem;
font-family: monospace;
font-size: 0.85em;
border: 1px solid var(--nord4);
border: 1px solid var(--color-border);
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .help-text code {
background-color: var(--nord2);
border-color: var(--nord3);
}
}
:global(:root[data-theme="dark"]) .help-text code {
background-color: var(--nord2);
border-color: var(--nord3);
}
.help-text ul {
margin: 0.5rem 0;
padding-left: 1rem;
@@ -979,7 +790,7 @@
}
.execution-preview {
background-color: var(--nord8);
background-color: var(--color-bg-tertiary);
border: 1px solid var(--blue);
border-radius: 0.5rem;
padding: 1rem;
@@ -1000,28 +811,12 @@
}
.frequency-description {
color: var(--nord2);
color: var(--color-text-secondary);
font-size: 0.9rem;
margin: 0;
font-style: italic;
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .execution-preview {
background-color: var(--nord2);
}
:global(:root:not([data-theme="light"])) .frequency-description {
color: var(--nord4);
}
}
:global(:root[data-theme="dark"]) .execution-preview {
background-color: var(--nord2);
}
:global(:root[data-theme="dark"]) .frequency-description {
color: var(--nord4);
}
/* Amount-currency styling */
.amount-currency {
display: flex;
@@ -1050,21 +845,21 @@
}
.conversion-preview.loading {
background-color: var(--nord8);
background-color: var(--color-bg-tertiary);
border-color: var(--blue);
color: var(--blue);
}
.conversion-preview.error {
background-color: var(--nord6);
background-color: var(--color-bg-secondary);
border-color: var(--red);
color: var(--red);
}
.conversion-preview.success {
background-color: var(--nord14);
background-color: var(--color-bg-tertiary);
border-color: var(--green);
color: var(--nord0);
color: var(--color-text-primary);
}
.conversion-preview small {
@@ -1072,31 +867,6 @@
font-weight: 500;
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .conversion-preview.loading {
background-color: var(--nord2);
}
:global(:root:not([data-theme="light"])) .conversion-preview.error {
background-color: var(--accent-dark);
}
:global(:root:not([data-theme="light"])) .conversion-preview.success {
background-color: var(--nord2);
color: var(--font-default-dark);
}
}
:global(:root[data-theme="dark"]) .conversion-preview.loading {
background-color: var(--nord2);
}
:global(:root[data-theme="dark"]) .conversion-preview.error {
background-color: var(--accent-dark);
}
:global(:root[data-theme="dark"]) .conversion-preview.success {
background-color: var(--nord2);
color: var(--font-default-dark);
}
@media (max-width: 600px) {
.add-payment {
padding: 1rem;
@@ -1106,10 +876,6 @@
grid-template-columns: 1fr;
}
.form-actions {
flex-direction: column;
}
.amount-currency {
flex-direction: column;
}
@@ -1119,4 +885,5 @@
flex: none;
}
}
</style>
@@ -4,6 +4,7 @@
import { getCategoryOptions } from '$lib/utils/categories';
import FormSection from '$lib/components/FormSection.svelte';
import ImageUpload from '$lib/components/ImageUpload.svelte';
import SaveFab from '$lib/components/SaveFab.svelte';
/**
* @typedef {import('$models/Payment').IPayment & {splits?: import('$models/PaymentSplit').IPaymentSplit[]}} PaymentWithSplits
@@ -582,15 +583,9 @@
>
{deleting ? 'Deleting...' : 'Delete Payment'}
</button>
<div class="main-actions">
<button type="button" class="btn-secondary" onclick={() => goto('/cospend/payments')}>
Cancel
</button>
<button type="submit" class="btn-primary" disabled={saving || deleting}>
{saving ? 'Saving...' : 'Save Changes'}
</button>
</div>
</div>
<SaveFab disabled={saving || deleting} label="Save changes" />
</form>
{/if}
</main>
@@ -609,32 +604,16 @@
.header h1 {
margin: 0 0 0.5rem 0;
color: var(--nord0);
color: var(--color-text-primary);
font-size: 2rem;
}
.header p {
margin: 0;
color: var(--nord3);
color: var(--color-text-secondary);
font-size: 1.1rem;
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .header h1 {
color: var(--font-default-dark);
}
:global(:root:not([data-theme="light"])) .header p {
color: var(--nord4);
}
}
:global(:root[data-theme="dark"]) .header h1 {
color: var(--font-default-dark);
}
:global(:root[data-theme="dark"]) .header p {
color: var(--nord4);
}
.loading, .error {
text-align: center;
padding: 2rem;
@@ -643,20 +622,11 @@
.error {
color: var(--red);
background-color: var(--nord6);
background-color: var(--color-bg-secondary);
border-radius: 0.5rem;
border: 1px solid var(--red);
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .error {
background-color: var(--accent-dark);
}
}
:global(:root[data-theme="dark"]) .error {
background-color: var(--accent-dark);
}
.payment-form {
display: flex;
flex-direction: column;
@@ -677,27 +647,18 @@
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: var(--nord2);
color: var(--color-text-secondary);
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) label {
color: var(--nord5);
}
}
:global(:root[data-theme="dark"]) label {
color: var(--nord5);
}
input, textarea, select {
width: 100%;
padding: 0.75rem;
border: 1px solid var(--nord4);
border: 1px solid var(--color-border);
border-radius: 0.5rem;
font-size: 1rem;
box-sizing: border-box;
background-color: var(--nord6);
color: var(--nord0);
background-color: var(--color-bg-tertiary);
color: var(--color-text-primary);
}
input:focus, textarea:focus, select:focus {
@@ -710,48 +671,20 @@
cursor: pointer;
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) input,
:global(:root:not([data-theme="light"])) textarea,
:global(:root:not([data-theme="light"])) select {
background-color: var(--nord2);
color: var(--font-default-dark);
border-color: var(--nord3);
}
}
:global(:root[data-theme="dark"]) input,
:global(:root[data-theme="dark"]) textarea,
:global(:root[data-theme="dark"]) select {
background-color: var(--nord2);
color: var(--font-default-dark);
border-color: var(--nord3);
}
.split-method-info {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 1rem;
padding: 0.75rem;
background-color: var(--nord14);
background-color: var(--color-bg-tertiary);
border-radius: 0.5rem;
border: 1px solid var(--green);
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .split-method-info {
background-color: var(--nord2);
border-color: var(--nord3);
}
}
:global(:root[data-theme="dark"]) .split-method-info {
background-color: var(--nord2);
border-color: var(--nord3);
}
.split-method-info .label {
font-weight: 600;
color: var(--nord1);
color: var(--color-text-secondary);
}
.split-method-info .value {
@@ -759,66 +692,28 @@
font-weight: 500;
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .split-method-info .label {
color: var(--nord5);
}
}
:global(:root[data-theme="dark"]) .split-method-info .label {
color: var(--nord5);
}
.personal-amounts-editor {
margin-bottom: 1.5rem;
padding: 1rem;
background-color: var(--nord5);
background-color: var(--color-bg-tertiary);
border-radius: 0.5rem;
border: 1px solid var(--nord4);
border: 1px solid var(--color-border);
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .personal-amounts-editor {
background-color: var(--nord2);
border-color: var(--nord3);
}
}
:global(:root[data-theme="dark"]) .personal-amounts-editor {
background-color: var(--nord2);
border-color: var(--nord3);
}
.personal-amounts-editor h3 {
margin-top: 0;
margin-bottom: 0.5rem;
color: var(--nord0);
color: var(--color-text-primary);
font-size: 1rem;
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .personal-amounts-editor h3 {
color: var(--font-default-dark);
}
}
:global(:root[data-theme="dark"]) .personal-amounts-editor h3 {
color: var(--font-default-dark);
}
.personal-amounts-editor .description {
color: var(--nord2);
color: var(--color-text-secondary);
font-size: 0.9rem;
margin-bottom: 1rem;
font-style: italic;
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .personal-amounts-editor .description {
color: var(--nord4);
}
}
:global(:root[data-theme="dark"]) .personal-amounts-editor .description {
color: var(--nord4);
}
.personal-input {
display: flex;
align-items: center;
@@ -835,11 +730,11 @@
.personal-input input {
max-width: 150px;
padding: 0.5rem;
border: 1px solid var(--nord4);
border: 1px solid var(--color-border);
border-radius: 0.5rem;
font-size: 1rem;
background-color: var(--nord6);
color: var(--nord0);
background-color: var(--color-bg-tertiary);
color: var(--color-text-primary);
}
.personal-input input:focus {
@@ -848,68 +743,26 @@
box-shadow: 0 0 0 2px rgba(94, 129, 172, 0.2);
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .personal-input input {
background-color: var(--nord1);
color: var(--font-default-dark);
border-color: var(--nord3);
}
}
:global(:root[data-theme="dark"]) .personal-input input {
background-color: var(--nord1);
color: var(--font-default-dark);
border-color: var(--nord3);
}
.remainder-info {
margin-top: 1rem;
padding: 0.75rem;
background-color: var(--nord14);
background-color: var(--color-bg-tertiary);
border-radius: 0.5rem;
border: 1px solid var(--green);
}
.remainder-info.error {
background-color: var(--nord6);
background-color: var(--color-bg-secondary);
border-color: var(--red);
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .remainder-info {
background-color: var(--nord1);
border-color: var(--nord3);
}
:global(:root:not([data-theme="light"])) .remainder-info.error {
background-color: var(--accent-dark);
border-color: var(--red);
}
}
:global(:root[data-theme="dark"]) .remainder-info {
background-color: var(--nord1);
border-color: var(--nord3);
}
:global(:root[data-theme="dark"]) .remainder-info.error {
background-color: var(--accent-dark);
border-color: var(--red);
}
.remainder-info span {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: var(--nord0);
color: var(--color-text-primary);
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .remainder-info span {
color: var(--font-default-dark);
}
}
:global(:root[data-theme="dark"]) .remainder-info span {
color: var(--font-default-dark);
}
.error-message {
color: var(--red);
font-weight: 600;
@@ -927,54 +780,25 @@
.splits-display h3 {
margin-top: 0;
margin-bottom: 1rem;
color: var(--nord0);
color: var(--color-text-primary);
font-size: 1rem;
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .splits-display h3 {
color: var(--font-default-dark);
}
}
:global(:root[data-theme="dark"]) .splits-display h3 {
color: var(--font-default-dark);
}
.split-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem;
background-color: var(--nord5);
background-color: var(--color-bg-tertiary);
border-radius: 0.5rem;
border: 1px solid var(--nord4);
border: 1px solid var(--color-border);
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .split-item {
background-color: var(--nord2);
border-color: var(--nord3);
}
}
:global(:root[data-theme="dark"]) .split-item {
background-color: var(--nord2);
border-color: var(--nord3);
}
.split-username {
font-weight: 500;
color: var(--nord0);
color: var(--color-text-primary);
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .split-username {
color: var(--font-default-dark);
}
}
:global(:root[data-theme="dark"]) .split-username {
color: var(--font-default-dark);
}
.split-amount.positive {
color: var(--green);
font-weight: 500;
@@ -986,21 +810,12 @@
}
.note {
color: var(--nord2);
color: var(--color-text-secondary);
font-size: 0.9rem;
font-style: italic;
margin: 0;
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .note {
color: var(--nord4);
}
}
:global(:root[data-theme="dark"]) .note {
color: var(--nord4);
}
.js-only {
display: none;
}
@@ -1024,68 +839,12 @@
gap: 1rem;
}
.main-actions {
display: flex;
gap: 1rem;
}
.btn-primary, .btn-secondary, .btn-danger {
.btn-danger {
padding: 0.75rem 1.5rem;
border-radius: 0.5rem;
font-size: 1rem;
cursor: pointer;
transition: all 0.2s;
}
.btn-primary {
background-color: var(--blue);
color: white;
border: none;
}
.btn-primary:hover:not(:disabled) {
background-color: var(--nord10);
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}
.btn-primary:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-secondary {
background-color: var(--nord5);
color: var(--nord0);
border: 1px solid var(--nord4);
}
.btn-secondary:hover {
background-color: var(--nord4);
transform: translateY(-1px);
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .btn-secondary {
background-color: var(--nord2);
color: var(--font-default-dark);
border-color: var(--nord3);
}
:global(:root:not([data-theme="light"])) .btn-secondary:hover {
background-color: var(--nord3);
}
}
:global(:root[data-theme="dark"]) .btn-secondary {
background-color: var(--nord2);
color: var(--font-default-dark);
border-color: var(--nord3);
}
:global(:root[data-theme="dark"]) .btn-secondary:hover {
background-color: var(--nord3);
}
.btn-danger {
background-color: var(--red);
color: white;
border: none;
@@ -1129,21 +888,21 @@
}
.conversion-preview.loading {
background-color: var(--nord8);
background-color: var(--color-bg-tertiary);
border-color: var(--blue);
color: var(--blue);
}
.conversion-preview.error {
background-color: var(--nord6);
background-color: var(--color-bg-secondary);
border-color: var(--red);
color: var(--red);
}
.conversion-preview.success {
background-color: var(--nord14);
background-color: var(--color-bg-tertiary);
border-color: var(--green);
color: var(--nord0);
color: var(--color-text-primary);
}
.conversion-preview small {
@@ -1155,42 +914,10 @@
display: block;
margin-top: 0.25rem;
font-size: 0.8rem;
color: var(--nord3);
color: var(--color-text-secondary);
font-style: italic;
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .conversion-preview.loading {
background-color: var(--nord2);
}
:global(:root:not([data-theme="light"])) .conversion-preview.error {
background-color: var(--accent-dark);
}
:global(:root:not([data-theme="light"])) .conversion-preview.success {
background-color: var(--nord2);
color: var(--font-default-dark);
}
:global(:root:not([data-theme="light"])) .help-text {
color: var(--nord4);
}
}
:global(:root[data-theme="dark"]) .conversion-preview.loading {
background-color: var(--nord2);
}
:global(:root[data-theme="dark"]) .conversion-preview.error {
background-color: var(--accent-dark);
}
:global(:root[data-theme="dark"]) .conversion-preview.success {
background-color: var(--nord2);
color: var(--font-default-dark);
}
:global(:root[data-theme="dark"]) .help-text {
color: var(--nord4);
}
@media (max-width: 600px) {
.edit-payment {
padding: 1rem;
@@ -1232,4 +959,5 @@
max-width: none;
}
}
</style>
@@ -4,7 +4,7 @@
import ProfilePicture from '$lib/components/cospend/ProfilePicture.svelte';
import { getCategoryEmoji, getCategoryName } from '$lib/utils/categories';
import EditButton from '$lib/components/EditButton.svelte';
import { formatCurrency } from '$lib/utils/formatters';
@@ -21,7 +21,7 @@
onMount(async () => {
// Mark that JavaScript is loaded
document.body.classList.add('js-loaded');
// Only refresh if we don't have server data
if (!payment) {
await loadPayment();
@@ -48,7 +48,7 @@
if (payment.currency === 'CHF' || !payment.originalAmount) {
return formatCurrency(payment.amount, 'CHF', 'de-CH');
}
return `${formatCurrency(payment.originalAmount, payment.currency, 'de-CH')} ≈ ${formatCurrency(payment.amount, 'CHF', 'de-CH')}`;
}
@@ -58,7 +58,7 @@
function getSplitDescription(/** @type {any} */ payment) {
if (!payment.splits || payment.splits.length === 0) return 'No splits';
if (payment.splitMethod === 'equal') {
return `Split equally among ${payment.splits.length} people`;
} else if (payment.splitMethod === 'full') {
@@ -182,8 +182,6 @@
padding: 2rem;
}
.loading, .error {
text-align: center;
padding: 2rem;
@@ -192,57 +190,27 @@
.error {
color: var(--red);
background-color: var(--nord6);
background-color: var(--color-bg-secondary);
border-radius: 0.5rem;
border: 1px solid var(--red);
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .error {
background-color: var(--accent-dark);
}
}
:global(:root[data-theme="dark"]) .error {
background-color: var(--accent-dark);
}
.payment-card {
background: var(--nord6);
background: var(--color-surface);
border-radius: 0.75rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
overflow: hidden;
border: 1px solid var(--nord4);
border: 1px solid var(--color-border);
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .payment-card {
background: var(--nord1);
border-color: var(--nord2);
}
}
:global(:root[data-theme="dark"]) .payment-card {
background: var(--nord1);
border-color: var(--nord2);
}
.payment-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
padding: 2rem;
background: linear-gradient(135deg, var(--nord5), var(--nord4));
border-bottom: 1px solid var(--nord3);
background: var(--color-bg-tertiary);
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .payment-header {
background: linear-gradient(135deg, var(--nord2), var(--nord3));
}
}
:global(:root[data-theme="dark"]) .payment-header {
background: linear-gradient(135deg, var(--nord2), var(--nord3));
}
.title-with-category {
display: flex;
align-items: center;
@@ -257,7 +225,7 @@
.title-section h1 {
margin: 0;
color: var(--nord0);
color: var(--color-text-primary);
font-size: 1.75rem;
}
@@ -267,15 +235,6 @@
color: var(--blue);
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .title-section h1 {
color: var(--font-default-dark);
}
}
:global(:root[data-theme="dark"]) .title-section h1 {
color: var(--font-default-dark);
}
.receipt-image {
flex-shrink: 0;
margin-left: 2rem;
@@ -286,18 +245,9 @@
max-height: 150px;
object-fit: cover;
border-radius: 0.5rem;
border: 1px solid var(--nord4);
border: 1px solid var(--color-border);
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .receipt-image img {
border-color: var(--nord2);
}
}
:global(:root[data-theme="dark"]) .receipt-image img {
border-color: var(--nord2);
}
.payment-info {
padding: 2rem;
}
@@ -317,100 +267,43 @@
.label {
font-weight: 600;
color: var(--nord3);
color: var(--color-text-secondary);
font-size: 0.9rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.value {
color: var(--nord0);
color: var(--color-text-primary);
font-size: 1rem;
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .label {
color: var(--nord4);
}
:global(:root:not([data-theme="light"])) .value {
color: var(--font-default-dark);
}
}
:global(:root[data-theme="dark"]) .label {
color: var(--nord4);
}
:global(:root[data-theme="dark"]) .value {
color: var(--font-default-dark);
}
.description {
border-top: 1px solid var(--nord4);
padding-top: 1.5rem;
}
.description h3 {
margin: 0 0 0.75rem 0;
color: var(--nord0);
color: var(--color-text-primary);
font-size: 1.1rem;
}
.description p {
margin: 0;
color: var(--nord2);
color: var(--color-text-tertiary);
line-height: 1.5;
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .description {
border-top-color: var(--nord2);
}
:global(:root:not([data-theme="light"])) .description h3 {
color: var(--font-default-dark);
}
:global(:root:not([data-theme="light"])) .description p {
color: var(--nord5);
}
}
:global(:root[data-theme="dark"]) .description {
border-top-color: var(--nord2);
}
:global(:root[data-theme="dark"]) .description h3 {
color: var(--font-default-dark);
}
:global(:root[data-theme="dark"]) .description p {
color: var(--nord5);
}
.splits-section {
border-top: 1px solid var(--nord4);
padding: 2rem;
}
.splits-section h3 {
margin: 0 0 1rem 0;
color: var(--nord0);
color: var(--color-text-primary);
font-size: 1.1rem;
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .splits-section {
border-top-color: var(--nord2);
}
:global(:root:not([data-theme="light"])) .splits-section h3 {
color: var(--font-default-dark);
}
}
:global(:root[data-theme="dark"]) .splits-section {
border-top-color: var(--nord2);
}
:global(:root[data-theme="dark"]) .splits-section h3 {
color: var(--font-default-dark);
}
.splits-list {
display: flex;
flex-direction: column;
@@ -422,36 +315,14 @@
justify-content: space-between;
align-items: center;
padding: 1rem;
background: var(--nord5);
background: var(--color-bg-primary);
border-radius: 0.5rem;
border: 1px solid var(--nord4);
}
.split-item.current-user {
background: var(--nord8);
border-color: var(--blue);
background: var(--color-bg-tertiary);
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .split-item {
background: var(--nord2);
border-color: var(--nord3);
}
:global(:root:not([data-theme="light"])) .split-item.current-user {
background: var(--nord3);
border-color: var(--blue);
}
}
:global(:root[data-theme="dark"]) .split-item {
background: var(--nord2);
border-color: var(--nord3);
}
:global(:root[data-theme="dark"]) .split-item.current-user {
background: var(--nord3);
border-color: var(--blue);
}
.split-user {
display: flex;
align-items: center;
@@ -466,7 +337,7 @@
.username {
font-weight: 500;
color: var(--nord0);
color: var(--color-text-primary);
}
.you-badge {
@@ -478,15 +349,6 @@
font-weight: 500;
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .username {
color: var(--font-default-dark);
}
}
:global(:root[data-theme="dark"]) .username {
color: var(--font-default-dark);
}
.split-amount {
font-weight: 500;
font-size: 1rem;
@@ -502,7 +364,7 @@
.exchange-rate-info {
margin-top: 0.5rem;
color: var(--nord3);
color: var(--color-text-secondary);
font-style: italic;
}
@@ -510,15 +372,6 @@
font-size: 0.8rem;
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .exchange-rate-info {
color: var(--nord4);
}
}
:global(:root[data-theme="dark"]) .exchange-rate-info {
color: var(--nord4);
}
@media (max-width: 600px) {
.payment-view {
padding: 1rem;
@@ -544,4 +397,4 @@
gap: 0.5rem;
}
}
</style>
</style>
+31 -210
View File
@@ -6,6 +6,7 @@
import { toast } from '$lib/js/toast.svelte';
import AddButton from '$lib/components/AddButton.svelte';
import { formatCurrency } from '$lib/utils/formatters';
import Toggle from '$lib/components/Toggle.svelte';
let { data } = $props();
@@ -102,8 +103,8 @@
<div class="filters">
<label>
<input type="checkbox" bind:checked={showActiveOnly} />
Show active only
<Toggle bind:checked={showActiveOnly} />
<span>Show active only</span>
</label>
</div>
@@ -143,7 +144,7 @@
<span class="label">Category:</span>
<span class="value">{getCategoryName(payment.category)}</span>
</div>
<div class="detail-row">
<span class="label">Frequency:</span>
<span class="value">{getFrequencyDescription(payment)}</span>
@@ -241,68 +242,33 @@
.header h1 {
margin: 0 0 0.5rem 0;
color: var(--nord0);
color: var(--color-text-primary);
font-size: 2rem;
}
.header p {
margin: 0;
color: var(--nord3);
color: var(--color-text-secondary);
font-size: 1.1rem;
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .header h1 {
color: var(--font-default-dark);
}
:global(:root:not([data-theme="light"])) .header p {
color: var(--nord4);
}
}
:global(:root[data-theme="dark"]) .header h1 {
color: var(--font-default-dark);
}
:global(:root[data-theme="dark"]) .header p {
color: var(--nord4);
}
.filters {
margin-bottom: 1.5rem;
padding: 1rem;
background: var(--nord6);
background: var(--color-surface);
border-radius: 0.5rem;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
border: 1px solid var(--nord4);
border: 1px solid var(--color-border);
}
.filters label {
display: flex;
align-items: center;
gap: 0.5rem;
gap: 0.75rem;
cursor: pointer;
font-weight: 500;
color: var(--nord0);
color: var(--color-text-primary);
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .filters {
background: var(--nord1);
border-color: var(--nord2);
}
:global(:root:not([data-theme="light"])) .filters label {
color: var(--font-default-dark);
}
}
:global(:root[data-theme="dark"]) .filters {
background: var(--nord1);
border-color: var(--nord2);
}
:global(:root[data-theme="dark"]) .filters label {
color: var(--font-default-dark);
}
.loading, .error {
text-align: center;
padding: 2rem;
@@ -311,67 +277,32 @@
.error {
color: var(--red);
background-color: var(--nord6);
background-color: var(--color-bg-secondary);
border-radius: 0.5rem;
border: 1px solid var(--red);
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .error {
background-color: var(--accent-dark);
}
}
:global(:root[data-theme="dark"]) .error {
background-color: var(--accent-dark);
}
.empty-state {
text-align: center;
padding: 4rem 2rem;
background: var(--nord6);
background: var(--color-surface);
border-radius: 0.75rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
border: 1px solid var(--nord4);
border: 1px solid var(--color-border);
}
.empty-state h2 {
margin-bottom: 1rem;
color: var(--nord0);
color: var(--color-text-primary);
}
.empty-state p {
color: var(--nord2);
color: var(--color-text-secondary);
margin-bottom: 2rem;
max-width: 500px;
margin-left: auto;
margin-right: auto;
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .empty-state {
background: var(--nord1);
border-color: var(--nord2);
}
:global(:root:not([data-theme="light"])) .empty-state h2 {
color: var(--font-default-dark);
}
:global(:root:not([data-theme="light"])) .empty-state p {
color: var(--nord4);
}
}
:global(:root[data-theme="dark"]) .empty-state {
background: var(--nord1);
border-color: var(--nord2);
}
:global(:root[data-theme="dark"]) .empty-state h2 {
color: var(--font-default-dark);
}
:global(:root[data-theme="dark"]) .empty-state p {
color: var(--nord4);
}
.payments-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
@@ -379,53 +310,24 @@
}
.payment-card {
background: var(--nord6);
background: var(--color-surface);
border-radius: 0.75rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
padding: 1.5rem;
transition: all 0.2s;
border: 1px solid var(--nord4);
border: 1px solid var(--color-border);
}
.payment-card:hover {
transform: translateY(-1px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
border-color: var(--nord3);
}
.payment-card.inactive {
opacity: 0.7;
background: var(--nord5);
border-color: var(--nord3);
background: var(--color-bg-tertiary);
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .payment-card {
background: var(--nord1);
border-color: var(--nord2);
}
:global(:root:not([data-theme="light"])) .payment-card:hover {
border-color: var(--nord3);
}
:global(:root:not([data-theme="light"])) .payment-card.inactive {
background: var(--nord2);
border-color: var(--nord3);
}
}
:global(:root[data-theme="dark"]) .payment-card {
background: var(--nord1);
border-color: var(--nord2);
}
:global(:root[data-theme="dark"]) .payment-card:hover {
border-color: var(--nord3);
}
:global(:root[data-theme="dark"]) .payment-card.inactive {
background: var(--nord2);
border-color: var(--nord3);
}
.card-header {
display: flex;
justify-content: space-between;
@@ -446,19 +348,10 @@
.payment-title h3 {
margin: 0;
color: var(--nord0);
color: var(--color-text-primary);
font-size: 1.25rem;
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .payment-title h3 {
color: var(--font-default-dark);
}
}
:global(:root[data-theme="dark"]) .payment-title h3 {
color: var(--font-default-dark);
}
.status-badge {
padding: 0.25rem 0.75rem;
border-radius: 1rem;
@@ -485,20 +378,11 @@
}
.payment-description {
color: var(--nord2);
color: var(--color-text-tertiary);
margin-bottom: 1rem;
font-style: italic;
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .payment-description {
color: var(--nord5);
}
}
:global(:root[data-theme="dark"]) .payment-description {
color: var(--nord5);
}
.payment-details {
margin-bottom: 1.5rem;
}
@@ -513,12 +397,12 @@
.label {
font-weight: 500;
color: var(--nord3);
color: var(--color-text-secondary);
font-size: 0.9rem;
}
.value {
color: var(--nord0);
color: var(--color-text-primary);
font-weight: 500;
}
@@ -527,22 +411,6 @@
font-weight: 600;
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .label {
color: var(--nord4);
}
:global(:root:not([data-theme="light"])) .value {
color: var(--font-default-dark);
}
}
:global(:root[data-theme="dark"]) .label {
color: var(--nord4);
}
:global(:root[data-theme="dark"]) .value {
color: var(--font-default-dark);
}
.payer-info {
display: flex;
align-items: center;
@@ -552,37 +420,19 @@
.splits-preview {
margin-bottom: 1.5rem;
padding: 1rem;
background-color: var(--nord5);
background-color: var(--color-bg-tertiary);
border-radius: 0.5rem;
border: 1px solid var(--nord4);
border: 1px solid var(--color-border);
}
.splits-preview h4 {
margin: 0 0 0.75rem 0;
font-size: 0.9rem;
color: var(--nord2);
color: var(--color-text-secondary);
text-transform: uppercase;
letter-spacing: 0.5px;
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .splits-preview {
background-color: var(--nord2);
border-color: var(--nord3);
}
:global(:root:not([data-theme="light"])) .splits-preview h4 {
color: var(--nord4);
}
}
:global(:root[data-theme="dark"]) .splits-preview {
background-color: var(--nord2);
border-color: var(--nord3);
}
:global(:root[data-theme="dark"]) .splits-preview h4 {
color: var(--nord4);
}
.splits-list {
display: flex;
flex-direction: column;
@@ -598,7 +448,7 @@
.split-item .username {
flex: 1;
font-weight: 500;
color: var(--nord0);
color: var(--color-text-primary);
}
.split-amount {
@@ -614,15 +464,6 @@
color: var(--red);
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .split-item .username {
color: var(--font-default-dark);
}
}
:global(:root[data-theme="dark"]) .split-item .username {
color: var(--font-default-dark);
}
.card-actions {
display: flex;
gap: 0.5rem;
@@ -657,35 +498,15 @@
}
.btn-secondary {
background-color: var(--nord5);
color: var(--nord0);
border: 1px solid var(--nord4);
background-color: var(--color-bg-tertiary);
color: var(--color-text-primary);
border: 1px solid var(--color-border);
}
.btn-secondary:hover {
background-color: var(--nord4);
background-color: var(--color-bg-elevated);
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .btn-secondary {
background-color: var(--nord2);
color: var(--font-default-dark);
border-color: var(--nord3);
}
:global(:root:not([data-theme="light"])) .btn-secondary:hover {
background-color: var(--nord3);
}
}
:global(:root[data-theme="dark"]) .btn-secondary {
background-color: var(--nord2);
color: var(--font-default-dark);
border-color: var(--nord3);
}
:global(:root[data-theme="dark"]) .btn-secondary:hover {
background-color: var(--nord3);
}
.btn-warning {
background-color: var(--orange);
color: white;
@@ -758,4 +579,4 @@
flex: 1;
}
}
</style>
</style>
@@ -7,6 +7,7 @@
import ProfilePicture from '$lib/components/cospend/ProfilePicture.svelte';
import SplitMethodSelector from '$lib/components/cospend/SplitMethodSelector.svelte';
import UsersList from '$lib/components/cospend/UsersList.svelte';
import SaveFab from '$lib/components/SaveFab.svelte';
let { data } = $props();
@@ -288,9 +289,6 @@
<main class="edit-recurring-payment">
<div class="header">
<h1>Edit Recurring Payment</h1>
<div class="header-actions">
<a href="/cospend/recurring" class="back-link">← Back to Recurring Payments</a>
</div>
</div>
{#if loadingPayment}
@@ -491,14 +489,7 @@
<div class="error">{error}</div>
{/if}
<div class="form-actions">
<button type="button" class="btn-secondary" onclick={() => goto('/cospend/recurring')}>
Cancel
</button>
<button type="submit" class="btn-primary" disabled={loading || cronError}>
{loading ? 'Saving...' : 'Save Changes'}
</button>
</div>
<SaveFab disabled={loading || cronError} label="Save changes" />
</form>
{/if}
</main>
@@ -519,12 +510,7 @@
.header h1 {
margin: 0;
color: var(--nord0);
}
.back-link {
color: var(--blue);
text-decoration: none;
color: var(--color-text-primary);
}
.loading {
@@ -540,17 +526,17 @@
}
.form-section {
background: var(--nord6);
background: var(--color-surface);
padding: 1.5rem;
border-radius: 0.75rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
border: 1px solid var(--nord4);
border: 1px solid var(--color-border);
}
.form-section h2 {
margin-top: 0;
margin-bottom: 1rem;
color: var(--nord0);
color: var(--color-text-primary);
font-size: 1.25rem;
}
@@ -568,18 +554,18 @@
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: var(--nord3);
color: var(--color-text-secondary);
}
input, textarea, select {
width: 100%;
padding: 0.75rem;
border: 1px solid var(--nord4);
border: 1px solid var(--color-border);
border-radius: 0.5rem;
font-size: 1rem;
box-sizing: border-box;
background: var(--nord5);
color: var(--nord0);
background: var(--color-bg-tertiary);
color: var(--color-text-primary);
}
input:focus, textarea:focus, select:focus {
@@ -594,16 +580,16 @@
.help-text {
margin-top: 0.5rem;
color: var(--nord3);
color: var(--color-text-secondary);
font-size: 0.9rem;
}
.help-text code {
background-color: var(--nord5);
background-color: var(--color-bg-tertiary);
padding: 0.125rem 0.25rem;
border-radius: 0.25rem;
font-family: monospace;
color: var(--nord0);
color: var(--color-text-primary);
}
.help-text ul {
@@ -622,7 +608,7 @@
}
.execution-preview {
background-color: var(--nord8);
background-color: var(--color-bg-tertiary);
border: 1px solid var(--blue);
border-radius: 0.5rem;
padding: 1rem;
@@ -643,17 +629,14 @@
}
.frequency-description {
color: var(--nord3);
color: var(--color-text-secondary);
font-size: 0.9rem;
margin: 0;
font-style: italic;
}
.error {
background-color: var(--nord6);
background-color: var(--color-bg-secondary);
color: var(--red);
padding: 1rem;
border-radius: 0.5rem;
@@ -661,177 +644,6 @@
border: 1px solid var(--red);
}
.form-actions {
display: flex;
gap: 1rem;
justify-content: flex-end;
}
.btn-primary, .btn-secondary {
padding: 0.75rem 1.5rem;
border-radius: 0.5rem;
font-size: 1rem;
cursor: pointer;
transition: all 0.2s;
}
.btn-primary {
background-color: var(--blue);
color: white;
border: none;
}
.btn-primary:hover:not(:disabled) {
background-color: var(--lightblue);
}
.btn-primary:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-secondary {
background-color: var(--nord5);
color: var(--nord0);
border: 1px solid var(--nord4);
}
.btn-secondary:hover {
background-color: var(--nord4);
}
@media (prefers-color-scheme: dark) {
:global(:root:not([data-theme="light"])) .header h1 {
color: var(--font-default-dark);
}
:global(:root:not([data-theme="light"])) .form-section {
background: var(--accent-dark);
border-color: var(--nord2);
}
:global(:root:not([data-theme="light"])) .form-section h2 {
color: var(--font-default-dark);
}
:global(:root:not([data-theme="light"])) label {
color: var(--nord4);
}
:global(:root:not([data-theme="light"])) input,
:global(:root:not([data-theme="light"])) textarea,
:global(:root:not([data-theme="light"])) select {
background: var(--nord1);
color: var(--font-default-dark);
border-color: var(--nord2);
}
:global(:root:not([data-theme="light"])) input:focus,
:global(:root:not([data-theme="light"])) textarea:focus,
:global(:root:not([data-theme="light"])) select:focus {
box-shadow: 0 0 0 2px rgba(136, 192, 208, 0.2);
}
:global(:root:not([data-theme="light"])) .help-text {
color: var(--nord4);
}
:global(:root:not([data-theme="light"])) .help-text code {
background-color: var(--nord1);
color: var(--font-default-dark);
}
:global(:root:not([data-theme="light"])) .execution-preview {
background-color: var(--nord2);
border-color: var(--blue);
}
:global(:root:not([data-theme="light"])) .error {
background-color: var(--accent-dark);
}
:global(:root:not([data-theme="light"])) .btn-secondary {
background-color: var(--nord1);
color: var(--font-default-dark);
border-color: var(--nord2);
}
:global(:root:not([data-theme="light"])) .btn-secondary:hover {
background-color: var(--nord2);
}
:global(:root:not([data-theme="light"])) .conversion-preview.loading {
background-color: var(--nord2);
}
:global(:root:not([data-theme="light"])) .conversion-preview.error {
background-color: var(--accent-dark);
}
:global(:root:not([data-theme="light"])) .conversion-preview.success {
background-color: var(--nord2);
color: var(--font-default-dark);
}
}
:global(:root[data-theme="dark"]) .header h1 {
color: var(--font-default-dark);
}
:global(:root[data-theme="dark"]) .form-section {
background: var(--accent-dark);
border-color: var(--nord2);
}
:global(:root[data-theme="dark"]) .form-section h2 {
color: var(--font-default-dark);
}
:global(:root[data-theme="dark"]) label {
color: var(--nord4);
}
:global(:root[data-theme="dark"]) input,
:global(:root[data-theme="dark"]) textarea,
:global(:root[data-theme="dark"]) select {
background: var(--nord1);
color: var(--font-default-dark);
border-color: var(--nord2);
}
:global(:root[data-theme="dark"]) input:focus,
:global(:root[data-theme="dark"]) textarea:focus,
:global(:root[data-theme="dark"]) select:focus {
box-shadow: 0 0 0 2px rgba(136, 192, 208, 0.2);
}
:global(:root[data-theme="dark"]) .help-text {
color: var(--nord4);
}
:global(:root[data-theme="dark"]) .help-text code {
background-color: var(--nord1);
color: var(--font-default-dark);
}
:global(:root[data-theme="dark"]) .execution-preview {
background-color: var(--nord2);
border-color: var(--blue);
}
:global(:root[data-theme="dark"]) .error {
background-color: var(--accent-dark);
}
:global(:root[data-theme="dark"]) .btn-secondary {
background-color: var(--nord1);
color: var(--font-default-dark);
border-color: var(--nord2);
}
:global(:root[data-theme="dark"]) .btn-secondary:hover {
background-color: var(--nord2);
}
:global(:root[data-theme="dark"]) .conversion-preview.loading {
background-color: var(--nord2);
}
:global(:root[data-theme="dark"]) .conversion-preview.error {
background-color: var(--accent-dark);
}
:global(:root[data-theme="dark"]) .conversion-preview.success {
background-color: var(--nord2);
color: var(--font-default-dark);
}
/* Amount-currency styling */
.amount-currency {
display: flex;
@@ -860,21 +672,21 @@
}
.conversion-preview.loading {
background-color: var(--nord8);
background-color: var(--color-bg-tertiary);
border-color: var(--blue);
color: var(--blue);
}
.conversion-preview.error {
background-color: var(--nord6);
background-color: var(--color-bg-secondary);
border-color: var(--red);
color: var(--red);
}
.conversion-preview.success {
background-color: var(--nord14);
background-color: var(--color-bg-tertiary);
border-color: var(--green);
color: var(--nord0);
color: var(--color-text-primary);
}
.conversion-preview small {
@@ -891,10 +703,6 @@
grid-template-columns: 1fr;
}
.form-actions {
flex-direction: column;
}
.amount-currency {
flex-direction: column;
}
@@ -904,4 +712,5 @@
flex: none;
}
}
</style>
@@ -1,22 +1,22 @@
<script>
import { page } from '$app/stores';
import { Pencil, Trash2 } from 'lucide-svelte';
import { Pencil, Trash2, ChevronDown } from 'lucide-svelte';
import { detectFitnessLang, t } from '$lib/js/fitnessI18n';
import { toast } from '$lib/js/toast.svelte';
const lang = $derived(detectFitnessLang($page.url.pathname));
const measureSlug = $derived(lang === 'en' ? 'measure' : 'messen');
import { getWorkout } from '$lib/js/workout.svelte';
import AddActionButton from '$lib/components/AddActionButton.svelte';
import AddButton from '$lib/components/AddButton.svelte';
let { data } = $props();
const workout = getWorkout();
let latest = $state(data.latest ? { ...data.latest } : {});
let measurements = $state(data.measurements?.measurements ? [...data.measurements.measurements] : []);
let showForm = $state(false);
let saving = $state(false);
// Profile fields (sex, height) — stored in FitnessGoal
let showProfile = $state(false);
let profileSex = $state(data.profile?.sex ?? 'male');
let profileHeight = $state(data.profile?.heightCm != null ? String(data.profile.heightCm) : '');
let profileSaving = $state(false);
@@ -48,27 +48,6 @@
profileSaving = false;
}
}
/** @type {string | null} */
let editingId = $state(null);
// Form fields
let formDate = $state('');
let formWeight = $state('');
let formBodyFat = $state('');
let formCalories = $state('');
let formNeck = $state('');
let formShoulders = $state('');
let formChest = $state('');
let formBicepsL = $state('');
let formBicepsR = $state('');
let formForearmsL = $state('');
let formForearmsR = $state('');
let formWaist = $state('');
let formHips = $state('');
let formThighsL = $state('');
let formThighsR = $state('');
let formCalvesL = $state('');
let formCalvesR = $state('');
const bodyPartFields = $derived([
{ label: t('neck', lang), key: 'neck', value: latest.measurements?.neck },
@@ -86,153 +65,6 @@
{ label: t('r_calf', lang), key: 'calvesRight', value: latest.measurements?.calves?.right }
]);
function resetForm() {
formDate = new Date().toISOString().slice(0, 10);
formWeight = '';
formBodyFat = '';
formCalories = '';
formNeck = '';
formShoulders = '';
formChest = '';
formBicepsL = '';
formBicepsR = '';
formForearmsL = '';
formForearmsR = '';
formWaist = '';
formHips = '';
formThighsL = '';
formThighsR = '';
formCalvesL = '';
formCalvesR = '';
editingId = null;
}
/** @param {any} m */
function populateForm(m) {
formDate = new Date(m.date).toISOString().slice(0, 10);
formWeight = m.weight != null ? String(m.weight) : '';
formBodyFat = m.bodyFatPercent != null ? String(m.bodyFatPercent) : '';
formCalories = m.caloricIntake != null ? String(m.caloricIntake) : '';
const bp = m.measurements ?? {};
formNeck = bp.neck != null ? String(bp.neck) : '';
formShoulders = bp.shoulders != null ? String(bp.shoulders) : '';
formChest = bp.chest != null ? String(bp.chest) : '';
const bl = bp.biceps?.left ?? bp.leftBicep;
const br = bp.biceps?.right ?? bp.rightBicep;
formBicepsL = bl != null ? String(bl) : '';
formBicepsR = br != null ? String(br) : '';
const fl = bp.forearms?.left ?? bp.leftForearm;
const fr = bp.forearms?.right ?? bp.rightForearm;
formForearmsL = fl != null ? String(fl) : '';
formForearmsR = fr != null ? String(fr) : '';
formWaist = bp.waist != null ? String(bp.waist) : '';
formHips = bp.hips != null ? String(bp.hips) : '';
const tl = bp.thighs?.left ?? bp.leftThigh;
const tr = bp.thighs?.right ?? bp.rightThigh;
formThighsL = tl != null ? String(tl) : '';
formThighsR = tr != null ? String(tr) : '';
const cl = bp.calves?.left ?? bp.leftCalf;
const cr = bp.calves?.right ?? bp.rightCalf;
formCalvesL = cl != null ? String(cl) : '';
formCalvesR = cr != null ? String(cr) : '';
}
/** @param {any} m */
function startEdit(m) {
populateForm(m);
editingId = m._id;
showForm = true;
}
function startAdd() {
resetForm();
editingId = null;
showForm = true;
}
function buildBody() {
/** @type {any} */
const body = { date: formDate };
if (formWeight) body.weight = Number(formWeight);
else body.weight = null;
if (formBodyFat) body.bodyFatPercent = Number(formBodyFat);
else body.bodyFatPercent = null;
if (formCalories) body.caloricIntake = Number(formCalories);
else body.caloricIntake = null;
/** @type {any} */
const m = {};
if (formNeck) m.neck = Number(formNeck);
if (formShoulders) m.shoulders = Number(formShoulders);
if (formChest) m.chest = Number(formChest);
if (formBicepsL || formBicepsR) m.biceps = {};
if (formBicepsL) m.biceps.left = Number(formBicepsL);
if (formBicepsR) m.biceps.right = Number(formBicepsR);
if (formForearmsL || formForearmsR) m.forearms = {};
if (formForearmsL) m.forearms.left = Number(formForearmsL);
if (formForearmsR) m.forearms.right = Number(formForearmsR);
if (formWaist) m.waist = Number(formWaist);
if (formHips) m.hips = Number(formHips);
if (formThighsL || formThighsR) m.thighs = {};
if (formThighsL) m.thighs.left = Number(formThighsL);
if (formThighsR) m.thighs.right = Number(formThighsR);
if (formCalvesL || formCalvesR) m.calves = {};
if (formCalvesL) m.calves.left = Number(formCalvesL);
if (formCalvesR) m.calves.right = Number(formCalvesR);
body.measurements = Object.keys(m).length > 0 ? m : null;
return body;
}
async function refreshLatest() {
try {
const latestRes = await fetch('/api/fitness/measurements/latest');
if (latestRes.ok) latest = await latestRes.json();
} catch {}
}
async function saveMeasurement() {
saving = true;
const body = buildBody();
try {
if (editingId) {
const res = await fetch(`/api/fitness/measurements/${editingId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
if (res.ok) {
const d = await res.json();
measurements = measurements.map((m) => m._id === editingId ? d.measurement : m);
await refreshLatest();
showForm = false;
resetForm();
} else {
const err = await res.json().catch(() => null);
toast.error(err?.error ?? 'Failed to save measurement');
}
} else {
const res = await fetch('/api/fitness/measurements', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
if (res.ok) {
const d = await res.json();
measurements = [d.measurement, ...measurements];
await refreshLatest();
showForm = false;
resetForm();
} else {
const err = await res.json().catch(() => null);
toast.error(err?.error ?? 'Failed to save measurement');
}
}
} catch { toast.error('Failed to save measurement'); }
saving = false;
}
/** @param {string} id */
async function deleteMeasurement(id) {
if (!confirm(t('delete_measurement_confirm', lang))) return;
@@ -240,11 +72,10 @@
const res = await fetch(`/api/fitness/measurements/${id}`, { method: 'DELETE' });
if (res.ok) {
measurements = measurements.filter((m) => m._id !== id);
await refreshLatest();
if (editingId === id) {
showForm = false;
resetForm();
}
try {
const latestRes = await fetch('/api/fitness/measurements/latest');
if (latestRes.ok) latest = await latestRes.json();
} catch {}
} else {
const err = await res.json().catch(() => null);
toast.error(err?.error ?? 'Failed to delete measurement');
@@ -275,88 +106,32 @@
<h1>{t('measure_title', lang)}</h1>
<section class="profile-section">
<h2>{t('profile', lang)}</h2>
<div class="profile-row">
<div class="form-group">
<label for="p-sex">{t('sex', lang)}</label>
<select id="p-sex" bind:value={profileSex}>
<option value="male">{t('male', lang)}</option>
<option value="female">{t('female', lang)}</option>
</select>
<button class="profile-toggle" onclick={() => showProfile = !showProfile}>
<h2>{t('profile', lang)}</h2>
<ChevronDown size={16} class={showProfile ? 'chevron open' : 'chevron'} />
</button>
{#if showProfile}
<div class="profile-row">
<div class="form-group">
<label for="p-sex">{t('sex', lang)}</label>
<select id="p-sex" bind:value={profileSex}>
<option value="male">{t('male', lang)}</option>
<option value="female">{t('female', lang)}</option>
</select>
</div>
<div class="form-group">
<label for="p-height">{t('height', lang)}</label>
<input id="p-height" type="number" min="100" max="250" placeholder="175" bind:value={profileHeight} />
</div>
{#if profileDirty}
<button class="profile-save-btn" onclick={saveProfile} disabled={profileSaving}>
{profileSaving ? t('saving', lang) : t('save', lang)}
</button>
{/if}
</div>
<div class="form-group">
<label for="p-height">{t('height', lang)}</label>
<input id="p-height" type="number" min="100" max="250" placeholder="175" bind:value={profileHeight} />
</div>
{#if profileDirty}
<button class="profile-save-btn" onclick={saveProfile} disabled={profileSaving}>
{profileSaving ? t('saving', lang) : t('save', lang)}
</button>
{/if}
</div>
{/if}
</section>
{#if showForm}
<form class="measure-form" onsubmit={(e) => { e.preventDefault(); saveMeasurement(); }}>
<div class="form-header">
<h2>{editingId ? t('edit_measurement', lang) : t('new_measurement', lang)}</h2>
<button type="button" class="cancel-form-btn" onclick={() => { showForm = false; resetForm(); }}>{t('cancel', lang)}</button>
</div>
<div class="form-group">
<label for="m-date">{t('date', lang)}</label>
<input id="m-date" type="date" bind:value={formDate} />
</div>
<h3>{t('general', lang)}</h3>
<div class="form-row">
<div class="form-group">
<label for="m-weight">{t('weight_kg', lang)}</label>
<input id="m-weight" type="number" step="0.1" bind:value={formWeight} placeholder="—" />
</div>
<div class="form-group">
<label for="m-bf">{t('body_fat_pct', lang)}</label>
<input id="m-bf" type="number" step="0.1" bind:value={formBodyFat} placeholder="—" />
</div>
<div class="form-group">
<label for="m-cal">{t('calories_kcal', lang)}</label>
<input id="m-cal" type="number" bind:value={formCalories} placeholder="—" />
</div>
</div>
<h3>{t('body_parts_cm', lang)}</h3>
<div class="form-row">
<div class="form-group"><label for="m-neck">{t('neck', lang)}</label><input id="m-neck" type="number" step="0.1" bind:value={formNeck} placeholder="—" /></div>
<div class="form-group"><label for="m-shoulders">{t('shoulders', lang)}</label><input id="m-shoulders" type="number" step="0.1" bind:value={formShoulders} placeholder="—" /></div>
<div class="form-group"><label for="m-chest">{t('chest', lang)}</label><input id="m-chest" type="number" step="0.1" bind:value={formChest} placeholder="—" /></div>
</div>
<div class="form-row">
<div class="form-group"><label for="m-bl">{t('l_bicep', lang)}</label><input id="m-bl" type="number" step="0.1" bind:value={formBicepsL} placeholder="—" /></div>
<div class="form-group"><label for="m-br">{t('r_bicep', lang)}</label><input id="m-br" type="number" step="0.1" bind:value={formBicepsR} placeholder="—" /></div>
</div>
<div class="form-row">
<div class="form-group"><label for="m-fl">{t('l_forearm', lang)}</label><input id="m-fl" type="number" step="0.1" bind:value={formForearmsL} placeholder="—" /></div>
<div class="form-group"><label for="m-fr">{t('r_forearm', lang)}</label><input id="m-fr" type="number" step="0.1" bind:value={formForearmsR} placeholder="—" /></div>
</div>
<div class="form-row">
<div class="form-group"><label for="m-waist">{t('waist', lang)}</label><input id="m-waist" type="number" step="0.1" bind:value={formWaist} placeholder="—" /></div>
<div class="form-group"><label for="m-hips">{t('hips', lang)}</label><input id="m-hips" type="number" step="0.1" bind:value={formHips} placeholder="—" /></div>
</div>
<div class="form-row">
<div class="form-group"><label for="m-tl">{t('l_thigh', lang)}</label><input id="m-tl" type="number" step="0.1" bind:value={formThighsL} placeholder="—" /></div>
<div class="form-group"><label for="m-tr">{t('r_thigh', lang)}</label><input id="m-tr" type="number" step="0.1" bind:value={formThighsR} placeholder="—" /></div>
</div>
<div class="form-row">
<div class="form-group"><label for="m-cl">{t('l_calf', lang)}</label><input id="m-cl" type="number" step="0.1" bind:value={formCalvesL} placeholder="—" /></div>
<div class="form-group"><label for="m-cr">{t('r_calf', lang)}</label><input id="m-cr" type="number" step="0.1" bind:value={formCalvesR} placeholder="—" /></div>
</div>
<button type="submit" class="save-btn" disabled={saving}>
{saving ? t('saving', lang) : editingId ? t('update_measurement', lang) : t('save_measurement', lang)}
</button>
</form>
{/if}
<section class="latest-section">
<h2>{t('latest', lang)}</h2>
<div class="stat-grid">
@@ -394,16 +169,16 @@
<h2>{t('history', lang)}</h2>
<div class="history-list">
{#each measurements as m (m._id)}
<div class="history-item" class:editing={editingId === m._id}>
<div class="history-item">
<div class="history-main">
<div class="history-info">
<span class="history-date">{formatDate(m.date)}</span>
<span class="history-summary">{summaryParts(m)}</span>
</div>
<div class="history-actions">
<button class="icon-btn edit" onclick={() => startEdit(m)} aria-label="Edit measurement">
<a class="icon-btn edit" href="/fitness/{measureSlug}/edit/{m._id}" aria-label="Edit measurement">
<Pencil size={14} />
</button>
</a>
<button class="icon-btn delete" onclick={() => deleteMeasurement(m._id)} aria-label="Delete measurement">
<Trash2 size={14} />
</button>
@@ -417,7 +192,7 @@
</div>
{#if !workout.active}
<AddActionButton onclick={startAdd} ariaLabel="Add measurement" />
<AddButton href="/fitness/{measureSlug}/add" />
{/if}
<style>
@@ -434,12 +209,6 @@
margin: 0 0 0.5rem;
font-size: 1.1rem;
}
h3 {
margin: 0.75rem 0 0.25rem;
font-size: 0.85rem;
color: var(--color-text-secondary);
}
/* Profile */
.profile-section {
background: var(--color-surface);
@@ -447,6 +216,27 @@
box-shadow: var(--shadow-sm);
padding: 0.75rem 1rem;
}
.profile-toggle {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
background: none;
border: none;
cursor: pointer;
padding: 0;
color: inherit;
}
.profile-toggle h2 {
margin: 0;
font-size: 0.9rem;
}
.profile-toggle :global(.chevron) {
transition: transform 0.2s;
}
.profile-toggle :global(.chevron.open) {
transform: rotate(180deg);
}
.profile-section h2 {
margin: 0 0 0.5rem;
font-size: 0.9rem;
@@ -455,14 +245,7 @@
display: flex;
gap: 0.75rem;
align-items: flex-end;
}
.profile-row select {
padding: 0.4rem 0.5rem;
border: 1px solid var(--color-border);
border-radius: 6px;
background: var(--color-bg-elevated);
color: inherit;
font-size: 0.85rem;
margin-top: 0.5rem;
}
.profile-save-btn {
padding: 0.4rem 0.75rem;
@@ -482,43 +265,6 @@
cursor: not-allowed;
}
/* Form */
.measure-form {
background: var(--color-surface);
border-radius: 8px;
box-shadow: var(--shadow-sm);
padding: 1rem;
}
.form-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
}
.form-header h2 {
margin: 0;
font-size: 1rem;
}
.cancel-form-btn {
background: none;
border: 1px solid var(--color-border);
border-radius: 6px;
color: var(--color-text-secondary);
padding: 0.3rem 0.75rem;
font-weight: 700;
font-size: 0.75rem;
cursor: pointer;
letter-spacing: 0.03em;
}
.cancel-form-btn:hover {
border-color: var(--color-text-primary);
color: var(--color-text-primary);
}
.form-row {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.form-group {
flex: 1;
display: flex;
@@ -533,7 +279,7 @@
text-transform: uppercase;
letter-spacing: 0.04em;
}
.form-group input {
.form-group input, .form-group select {
padding: 0.4rem 0.5rem;
border: 1px solid var(--color-border);
border-radius: 6px;
@@ -545,22 +291,6 @@
outline: none;
border-color: var(--color-primary);
}
.save-btn {
width: 100%;
margin-top: 0.75rem;
padding: 0.7rem;
background: var(--color-primary);
color: white;
border: none;
border-radius: 8px;
font-weight: 700;
font-size: 0.85rem;
cursor: pointer;
}
.save-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
/* Latest */
.stat-grid {
@@ -624,9 +354,6 @@
box-shadow: var(--shadow-sm);
padding: 0.6rem 0.75rem;
}
.history-item.editing {
border: 1px solid var(--color-primary);
}
.history-main {
display: flex;
justify-content: space-between;
@@ -682,8 +409,5 @@
.stat-grid {
grid-template-columns: 1fr;
}
.form-row {
flex-direction: column;
}
}
</style>
@@ -0,0 +1,200 @@
<script>
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { detectFitnessLang, t } from '$lib/js/fitnessI18n';
import { toast } from '$lib/js/toast.svelte';
import SaveFab from '$lib/components/SaveFab.svelte';
const lang = $derived(detectFitnessLang($page.url.pathname));
const measureSlug = $derived(lang === 'en' ? 'measure' : 'messen');
let saving = $state(false);
let formDate = $state(new Date().toISOString().slice(0, 10));
let formWeight = $state('');
let formBodyFat = $state('');
let formCalories = $state('');
let formNeck = $state('');
let formShoulders = $state('');
let formChest = $state('');
let formBicepsL = $state('');
let formBicepsR = $state('');
let formForearmsL = $state('');
let formForearmsR = $state('');
let formWaist = $state('');
let formHips = $state('');
let formThighsL = $state('');
let formThighsR = $state('');
let formCalvesL = $state('');
let formCalvesR = $state('');
function buildBody() {
/** @type {any} */
const body = { date: formDate };
if (formWeight) body.weight = Number(formWeight);
else body.weight = null;
if (formBodyFat) body.bodyFatPercent = Number(formBodyFat);
else body.bodyFatPercent = null;
if (formCalories) body.caloricIntake = Number(formCalories);
else body.caloricIntake = null;
/** @type {any} */
const m = {};
if (formNeck) m.neck = Number(formNeck);
if (formShoulders) m.shoulders = Number(formShoulders);
if (formChest) m.chest = Number(formChest);
if (formBicepsL || formBicepsR) m.biceps = {};
if (formBicepsL) m.biceps.left = Number(formBicepsL);
if (formBicepsR) m.biceps.right = Number(formBicepsR);
if (formForearmsL || formForearmsR) m.forearms = {};
if (formForearmsL) m.forearms.left = Number(formForearmsL);
if (formForearmsR) m.forearms.right = Number(formForearmsR);
if (formWaist) m.waist = Number(formWaist);
if (formHips) m.hips = Number(formHips);
if (formThighsL || formThighsR) m.thighs = {};
if (formThighsL) m.thighs.left = Number(formThighsL);
if (formThighsR) m.thighs.right = Number(formThighsR);
if (formCalvesL || formCalvesR) m.calves = {};
if (formCalvesL) m.calves.left = Number(formCalvesL);
if (formCalvesR) m.calves.right = Number(formCalvesR);
body.measurements = Object.keys(m).length > 0 ? m : null;
return body;
}
async function saveMeasurement() {
saving = true;
try {
const res = await fetch('/api/fitness/measurements', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(buildBody())
});
if (res.ok) {
await goto(`/fitness/${measureSlug}`);
} else {
const err = await res.json().catch(() => null);
toast.error(err?.error ?? 'Failed to save measurement');
}
} catch { toast.error('Failed to save measurement'); }
saving = false;
}
</script>
<svelte:head><title>{t('new_measurement', lang)} - Bocken</title></svelte:head>
<div class="measure-add">
<h1>{t('new_measurement', lang)}</h1>
<form onsubmit={(e) => { e.preventDefault(); saveMeasurement(); }}>
<div class="form-group">
<label for="m-date">{t('date', lang)}</label>
<input id="m-date" type="date" bind:value={formDate} />
</div>
<h3>{t('general', lang)}</h3>
<div class="form-row">
<div class="form-group">
<label for="m-weight">{t('weight_kg', lang)}</label>
<input id="m-weight" type="number" step="0.1" bind:value={formWeight} placeholder="--" />
</div>
<div class="form-group">
<label for="m-bf">{t('body_fat_pct', lang)}</label>
<input id="m-bf" type="number" step="0.1" bind:value={formBodyFat} placeholder="--" />
</div>
<div class="form-group">
<label for="m-cal">{t('calories_kcal', lang)}</label>
<input id="m-cal" type="number" bind:value={formCalories} placeholder="--" />
</div>
</div>
<h3>{t('body_parts_cm', lang)}</h3>
<div class="form-row">
<div class="form-group"><label for="m-neck">{t('neck', lang)}</label><input id="m-neck" type="number" step="0.1" bind:value={formNeck} placeholder="--" /></div>
<div class="form-group"><label for="m-shoulders">{t('shoulders', lang)}</label><input id="m-shoulders" type="number" step="0.1" bind:value={formShoulders} placeholder="--" /></div>
<div class="form-group"><label for="m-chest">{t('chest', lang)}</label><input id="m-chest" type="number" step="0.1" bind:value={formChest} placeholder="--" /></div>
</div>
<div class="form-row">
<div class="form-group"><label for="m-bl">{t('l_bicep', lang)}</label><input id="m-bl" type="number" step="0.1" bind:value={formBicepsL} placeholder="--" /></div>
<div class="form-group"><label for="m-br">{t('r_bicep', lang)}</label><input id="m-br" type="number" step="0.1" bind:value={formBicepsR} placeholder="--" /></div>
</div>
<div class="form-row">
<div class="form-group"><label for="m-fl">{t('l_forearm', lang)}</label><input id="m-fl" type="number" step="0.1" bind:value={formForearmsL} placeholder="--" /></div>
<div class="form-group"><label for="m-fr">{t('r_forearm', lang)}</label><input id="m-fr" type="number" step="0.1" bind:value={formForearmsR} placeholder="--" /></div>
</div>
<div class="form-row">
<div class="form-group"><label for="m-waist">{t('waist', lang)}</label><input id="m-waist" type="number" step="0.1" bind:value={formWaist} placeholder="--" /></div>
<div class="form-group"><label for="m-hips">{t('hips', lang)}</label><input id="m-hips" type="number" step="0.1" bind:value={formHips} placeholder="--" /></div>
</div>
<div class="form-row">
<div class="form-group"><label for="m-tl">{t('l_thigh', lang)}</label><input id="m-tl" type="number" step="0.1" bind:value={formThighsL} placeholder="--" /></div>
<div class="form-group"><label for="m-tr">{t('r_thigh', lang)}</label><input id="m-tr" type="number" step="0.1" bind:value={formThighsR} placeholder="--" /></div>
</div>
<div class="form-row">
<div class="form-group"><label for="m-cl">{t('l_calf', lang)}</label><input id="m-cl" type="number" step="0.1" bind:value={formCalvesL} placeholder="--" /></div>
<div class="form-group"><label for="m-cr">{t('r_calf', lang)}</label><input id="m-cr" type="number" step="0.1" bind:value={formCalvesR} placeholder="--" /></div>
</div>
<SaveFab disabled={saving} label={t('save_measurement', lang)} />
</form>
</div>
<style>
.measure-add {
display: flex;
flex-direction: column;
gap: 1rem;
}
h1 {
margin: 0;
font-size: 1.4rem;
}
h3 {
margin: 0.75rem 0 0.25rem;
font-size: 0.85rem;
color: var(--color-text-secondary);
}
form {
background: var(--color-surface);
border-radius: 8px;
box-shadow: var(--shadow-sm);
padding: 1rem;
}
.form-row {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.form-group {
flex: 1;
display: flex;
flex-direction: column;
gap: 0.2rem;
margin-bottom: 0.4rem;
}
.form-group label {
font-size: 0.7rem;
font-weight: 600;
color: var(--color-text-secondary);
text-transform: uppercase;
letter-spacing: 0.04em;
}
.form-group input {
padding: 0.4rem 0.5rem;
border: 1px solid var(--color-border);
border-radius: 6px;
background: var(--color-bg-elevated);
color: inherit;
font-size: 0.85rem;
}
.form-group input:focus {
outline: none;
border-color: var(--color-primary);
}
@media (max-width: 480px) {
.form-row {
flex-direction: column;
}
}
</style>
@@ -0,0 +1,7 @@
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ fetch, params }) => {
const res = await fetch(`/api/fitness/measurements/${params.id}`);
if (!res.ok) return { measurement: null };
return { measurement: await res.json() };
};
@@ -0,0 +1,256 @@
<script>
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { detectFitnessLang, t } from '$lib/js/fitnessI18n';
import { toast } from '$lib/js/toast.svelte';
import { Trash2 } from 'lucide-svelte';
import SaveFab from '$lib/components/SaveFab.svelte';
const lang = $derived(detectFitnessLang($page.url.pathname));
const measureSlug = $derived(lang === 'en' ? 'measure' : 'messen');
let { data } = $props();
const m = data.measurement?.measurement;
let saving = $state(false);
let deleting = $state(false);
// Populate form from loaded measurement
let formDate = $state(m ? new Date(m.date).toISOString().slice(0, 10) : '');
let formWeight = $state(m?.weight != null ? String(m.weight) : '');
let formBodyFat = $state(m?.bodyFatPercent != null ? String(m.bodyFatPercent) : '');
let formCalories = $state(m?.caloricIntake != null ? String(m.caloricIntake) : '');
const bp = m?.measurements ?? {};
let formNeck = $state(bp.neck != null ? String(bp.neck) : '');
let formShoulders = $state(bp.shoulders != null ? String(bp.shoulders) : '');
let formChest = $state(bp.chest != null ? String(bp.chest) : '');
let formBicepsL = $state((bp.biceps?.left ?? bp.leftBicep) != null ? String(bp.biceps?.left ?? bp.leftBicep) : '');
let formBicepsR = $state((bp.biceps?.right ?? bp.rightBicep) != null ? String(bp.biceps?.right ?? bp.rightBicep) : '');
let formForearmsL = $state((bp.forearms?.left ?? bp.leftForearm) != null ? String(bp.forearms?.left ?? bp.leftForearm) : '');
let formForearmsR = $state((bp.forearms?.right ?? bp.rightForearm) != null ? String(bp.forearms?.right ?? bp.rightForearm) : '');
let formWaist = $state(bp.waist != null ? String(bp.waist) : '');
let formHips = $state(bp.hips != null ? String(bp.hips) : '');
let formThighsL = $state((bp.thighs?.left ?? bp.leftThigh) != null ? String(bp.thighs?.left ?? bp.leftThigh) : '');
let formThighsR = $state((bp.thighs?.right ?? bp.rightThigh) != null ? String(bp.thighs?.right ?? bp.rightThigh) : '');
let formCalvesL = $state((bp.calves?.left ?? bp.leftCalf) != null ? String(bp.calves?.left ?? bp.leftCalf) : '');
let formCalvesR = $state((bp.calves?.right ?? bp.rightCalf) != null ? String(bp.calves?.right ?? bp.rightCalf) : '');
function buildBody() {
/** @type {any} */
const body = { date: formDate };
if (formWeight) body.weight = Number(formWeight);
else body.weight = null;
if (formBodyFat) body.bodyFatPercent = Number(formBodyFat);
else body.bodyFatPercent = null;
if (formCalories) body.caloricIntake = Number(formCalories);
else body.caloricIntake = null;
/** @type {any} */
const ms = {};
if (formNeck) ms.neck = Number(formNeck);
if (formShoulders) ms.shoulders = Number(formShoulders);
if (formChest) ms.chest = Number(formChest);
if (formBicepsL || formBicepsR) ms.biceps = {};
if (formBicepsL) ms.biceps.left = Number(formBicepsL);
if (formBicepsR) ms.biceps.right = Number(formBicepsR);
if (formForearmsL || formForearmsR) ms.forearms = {};
if (formForearmsL) ms.forearms.left = Number(formForearmsL);
if (formForearmsR) ms.forearms.right = Number(formForearmsR);
if (formWaist) ms.waist = Number(formWaist);
if (formHips) ms.hips = Number(formHips);
if (formThighsL || formThighsR) ms.thighs = {};
if (formThighsL) ms.thighs.left = Number(formThighsL);
if (formThighsR) ms.thighs.right = Number(formThighsR);
if (formCalvesL || formCalvesR) ms.calves = {};
if (formCalvesL) ms.calves.left = Number(formCalvesL);
if (formCalvesR) ms.calves.right = Number(formCalvesR);
body.measurements = Object.keys(ms).length > 0 ? ms : null;
return body;
}
async function saveMeasurement() {
saving = true;
try {
const res = await fetch(`/api/fitness/measurements/${m._id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(buildBody())
});
if (res.ok) {
await goto(`/fitness/${measureSlug}`);
} else {
const err = await res.json().catch(() => null);
toast.error(err?.error ?? 'Failed to save measurement');
}
} catch { toast.error('Failed to save measurement'); }
saving = false;
}
async function deleteMeasurement() {
if (!confirm(t('delete_measurement_confirm', lang))) return;
deleting = true;
try {
const res = await fetch(`/api/fitness/measurements/${m._id}`, { method: 'DELETE' });
if (res.ok) {
await goto(`/fitness/${measureSlug}`);
} else {
const err = await res.json().catch(() => null);
toast.error(err?.error ?? 'Failed to delete measurement');
}
} catch { toast.error('Failed to delete measurement'); }
deleting = false;
}
</script>
<svelte:head><title>{t('edit_measurement', lang)} - Bocken</title></svelte:head>
<div class="measure-edit">
<h1>{t('edit_measurement', lang)}</h1>
{#if !m}
<p>Measurement not found.</p>
{:else}
<form onsubmit={(e) => { e.preventDefault(); saveMeasurement(); }}>
<div class="form-group">
<label for="m-date">{t('date', lang)}</label>
<input id="m-date" type="date" bind:value={formDate} />
</div>
<h3>{t('general', lang)}</h3>
<div class="form-row">
<div class="form-group">
<label for="m-weight">{t('weight_kg', lang)}</label>
<input id="m-weight" type="number" step="0.1" bind:value={formWeight} placeholder="--" />
</div>
<div class="form-group">
<label for="m-bf">{t('body_fat_pct', lang)}</label>
<input id="m-bf" type="number" step="0.1" bind:value={formBodyFat} placeholder="--" />
</div>
<div class="form-group">
<label for="m-cal">{t('calories_kcal', lang)}</label>
<input id="m-cal" type="number" bind:value={formCalories} placeholder="--" />
</div>
</div>
<h3>{t('body_parts_cm', lang)}</h3>
<div class="form-row">
<div class="form-group"><label for="m-neck">{t('neck', lang)}</label><input id="m-neck" type="number" step="0.1" bind:value={formNeck} placeholder="--" /></div>
<div class="form-group"><label for="m-shoulders">{t('shoulders', lang)}</label><input id="m-shoulders" type="number" step="0.1" bind:value={formShoulders} placeholder="--" /></div>
<div class="form-group"><label for="m-chest">{t('chest', lang)}</label><input id="m-chest" type="number" step="0.1" bind:value={formChest} placeholder="--" /></div>
</div>
<div class="form-row">
<div class="form-group"><label for="m-bl">{t('l_bicep', lang)}</label><input id="m-bl" type="number" step="0.1" bind:value={formBicepsL} placeholder="--" /></div>
<div class="form-group"><label for="m-br">{t('r_bicep', lang)}</label><input id="m-br" type="number" step="0.1" bind:value={formBicepsR} placeholder="--" /></div>
</div>
<div class="form-row">
<div class="form-group"><label for="m-fl">{t('l_forearm', lang)}</label><input id="m-fl" type="number" step="0.1" bind:value={formForearmsL} placeholder="--" /></div>
<div class="form-group"><label for="m-fr">{t('r_forearm', lang)}</label><input id="m-fr" type="number" step="0.1" bind:value={formForearmsR} placeholder="--" /></div>
</div>
<div class="form-row">
<div class="form-group"><label for="m-waist">{t('waist', lang)}</label><input id="m-waist" type="number" step="0.1" bind:value={formWaist} placeholder="--" /></div>
<div class="form-group"><label for="m-hips">{t('hips', lang)}</label><input id="m-hips" type="number" step="0.1" bind:value={formHips} placeholder="--" /></div>
</div>
<div class="form-row">
<div class="form-group"><label for="m-tl">{t('l_thigh', lang)}</label><input id="m-tl" type="number" step="0.1" bind:value={formThighsL} placeholder="--" /></div>
<div class="form-group"><label for="m-tr">{t('r_thigh', lang)}</label><input id="m-tr" type="number" step="0.1" bind:value={formThighsR} placeholder="--" /></div>
</div>
<div class="form-row">
<div class="form-group"><label for="m-cl">{t('l_calf', lang)}</label><input id="m-cl" type="number" step="0.1" bind:value={formCalvesL} placeholder="--" /></div>
<div class="form-group"><label for="m-cr">{t('r_calf', lang)}</label><input id="m-cr" type="number" step="0.1" bind:value={formCalvesR} placeholder="--" /></div>
</div>
<div class="delete-actions">
<button type="button" class="btn-danger" onclick={deleteMeasurement} disabled={deleting || saving}>
<Trash2 size={14} />
{deleting ? t('saving', lang) : t('delete_', lang)}
</button>
</div>
<SaveFab disabled={saving || deleting} label={t('update_measurement', lang)} />
</form>
{/if}
</div>
<style>
.measure-edit {
display: flex;
flex-direction: column;
gap: 1rem;
}
h1 {
margin: 0;
font-size: 1.4rem;
}
h3 {
margin: 0.75rem 0 0.25rem;
font-size: 0.85rem;
color: var(--color-text-secondary);
}
form {
background: var(--color-surface);
border-radius: 8px;
box-shadow: var(--shadow-sm);
padding: 1rem;
}
.form-row {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.form-group {
flex: 1;
display: flex;
flex-direction: column;
gap: 0.2rem;
margin-bottom: 0.4rem;
}
.form-group label {
font-size: 0.7rem;
font-weight: 600;
color: var(--color-text-secondary);
text-transform: uppercase;
letter-spacing: 0.04em;
}
.form-group input {
padding: 0.4rem 0.5rem;
border: 1px solid var(--color-border);
border-radius: 6px;
background: var(--color-bg-elevated);
color: inherit;
font-size: 0.85rem;
}
.form-group input:focus {
outline: none;
border-color: var(--color-primary);
}
.delete-actions {
margin-top: 1rem;
}
.btn-danger {
display: inline-flex;
align-items: center;
gap: 0.4rem;
padding: 0.5rem 1rem;
background: var(--red);
color: white;
border: none;
border-radius: 6px;
font-size: 0.85rem;
cursor: pointer;
}
.btn-danger:hover:not(:disabled) {
background: var(--nord11);
}
.btn-danger:disabled {
opacity: 0.5;
cursor: not-allowed;
}
@media (max-width: 480px) {
.form-row {
flex-direction: column;
}
}
</style>