theming: migrate cospend to semantic CSS variables, extract SaveFab, refactor measure page
All checks were successful
CI / update (push) Successful in 4m21s

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 4f77f29a27
commit 7935ac6b75
21 changed files with 927 additions and 2568 deletions

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>

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>

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>

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;

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;

View File

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

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>

View File

@@ -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" />

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>

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>

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;

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>

View File

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

View File

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

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>

View File

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

View File

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

View File

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

View File

@@ -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() };
};

View File

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