feat: customizable dashboard layout (#32)

Users can now show/hide widgets and reorder them via a settings button
in the greeting header. Configuration is persisted server-side in
sync_config (dashboard_widgets key) and shared across all family members.

- Greeting widget gets a settings icon button opening a customize modal
- Modal lists all widgets (tasks, calendar, shopping, meals, notes,
  weather) with toggle switches and up/down reorder buttons
- Reset to default layout available in the modal
- GET /preferences now returns dashboard_widgets; PUT accepts it
- All 10 locales updated with new i18n keys
This commit is contained in:
Ulas
2026-04-14 08:04:26 +02:00
parent 6f532e45ec
commit 8f96e066f3
15 changed files with 495 additions and 50 deletions
+149
View File
@@ -98,10 +98,19 @@
grid-column: 1 / -1;
}
.widget-greeting__inner {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: var(--space-3);
}
.widget-greeting__content {
display: flex;
flex-direction: column;
gap: var(--space-0h);
flex: 1;
min-width: 0;
}
.widget-greeting__title {
@@ -1020,3 +1029,143 @@
.fab-action__btn:hover {
background-color: var(--color-accent-light);
}
/* --------------------------------------------------------
* Widget-Customize-Button (im Greeting-Header)
* -------------------------------------------------------- */
.widget-customize-btn {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border-radius: var(--radius-full);
background: rgba(255 255 255 / 0.18);
border: 1px solid rgba(255 255 255 / 0.3);
color: var(--color-text-on-accent);
cursor: pointer;
flex-shrink: 0;
transition: background-color var(--transition-fast);
}
.widget-customize-btn:hover,
.widget-customize-btn:focus-visible {
background: rgba(255 255 255 / 0.3);
outline: 2px solid rgba(255 255 255 / 0.5);
outline-offset: 2px;
}
/* --------------------------------------------------------
* Dashboard-Customize-Modal
* -------------------------------------------------------- */
.customize-list {
display: flex;
flex-direction: column;
gap: var(--space-1);
}
.customize-row {
display: flex;
align-items: center;
gap: var(--space-3);
padding: var(--space-2) var(--space-2);
border-radius: var(--radius-sm);
transition: background-color var(--transition-fast);
}
.customize-row:hover {
background-color: var(--color-surface-raised);
}
.customize-row__toggle {
position: relative;
display: inline-flex;
cursor: pointer;
flex-shrink: 0;
}
.customize-row__check {
position: absolute;
opacity: 0;
width: 0;
height: 0;
}
.customize-row__slider {
display: inline-block;
width: 36px;
height: 20px;
border-radius: var(--radius-full);
background-color: var(--color-border);
transition: background-color var(--transition-fast);
position: relative;
}
.customize-row__slider::after {
content: '';
position: absolute;
top: 2px;
left: 2px;
width: 16px;
height: 16px;
border-radius: 50%;
background: white;
transition: transform var(--transition-fast);
box-shadow: var(--shadow-xs);
}
.customize-row__check:checked + .customize-row__slider {
background-color: var(--color-accent);
}
.customize-row__check:checked + .customize-row__slider::after {
transform: translateX(16px);
}
.customize-row__check:focus-visible + .customize-row__slider {
outline: 2px solid var(--color-accent);
outline-offset: 2px;
}
.customize-row__icon {
width: 16px;
height: 16px;
flex-shrink: 0;
color: var(--color-text-secondary);
}
.customize-row__name {
flex: 1;
font-size: var(--text-sm);
font-weight: var(--font-weight-medium);
}
.customize-row__actions {
display: flex;
gap: var(--space-1);
flex-shrink: 0;
}
.customize-row__btn {
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border-radius: var(--radius-sm);
border: 1px solid var(--color-border);
background: transparent;
color: var(--color-text-secondary);
cursor: pointer;
transition: background-color var(--transition-fast), color var(--transition-fast);
}
.customize-row__btn:hover:not(:disabled) {
background-color: var(--color-surface-raised);
color: var(--color-text);
}
.customize-row__btn:disabled {
opacity: 0.3;
cursor: not-allowed;
}