diff --git a/frontend/src/client/event-types.ts b/frontend/src/client/event-types.ts index 1fd3b85..f33372d 100644 --- a/frontend/src/client/event-types.ts +++ b/frontend/src/client/event-types.ts @@ -16,7 +16,7 @@ // Backend contract: see internal/handlers/event_types.go and // internal/services/event_type_service.go. -import { t, tDyn, getLang } from "./i18n"; +import { t, tDyn, getLang, onLangChange } from "./i18n"; export interface EventType { id: string; @@ -491,6 +491,14 @@ export function attachEventTypeMultiSelectFilter( updateLabel(); })(); + // Trigger label and (when open) panel content come from t() — re-render + // when the language changes so "Alle Typen" / "All types" stays in sync + // with the active locale (t-paliad-117). + onLangChange(() => { + updateLabel(); + if (!panel.hidden) renderPanel(); + }); + return handle; } diff --git a/frontend/src/client/events.ts b/frontend/src/client/events.ts index 35d98f5..f4fc643 100644 --- a/frontend/src/client/events.ts +++ b/frontend/src/client/events.ts @@ -717,7 +717,7 @@ function applyTypeVisibility() { // Status filter is deadline-only. toggleFilterPair("events-filter-status", !isAppointment); // Event-type multi-select also deadline-only (appointments have no event_types). - toggleFilterPair("events-filter-event-type", !isAppointment, "events-filter-event-type-label"); + toggleFilterPair("events-filter-event-type", !isAppointment); // The panel is a popup the trigger owns via `panel.hidden`. Never stamp // `display: block` on it from the type filter — that overrides the // `.multi-panel[hidden]` CSS rule and leaves the panel visible on larger @@ -779,19 +779,13 @@ function toggleDisplay(id: string, show: boolean, displayWhenShown = "block") { el.style.display = show ? displayWhenShown : "none"; } -// toggleFilterPair shows/hides a control AND its sibling label so the -// filter row collapses cleanly when a filter doesn't apply for the -// current type. -function toggleFilterPair(controlID: string, show: boolean, labelID?: string) { - toggleDisplay(controlID, show, ""); - // Default: derive label by id="events-filter-X" → label htmlFor="events-filter-X" - const labelTarget = labelID ?? ""; - if (labelTarget) { - toggleDisplay(labelTarget, show, ""); - return; - } - const label = document.querySelector(`label[for="${controlID}"]`); - if (label) label.style.display = show ? "" : "none"; +// toggleFilterPair shows/hides the wrapping .filter-group so the label, +// control, and (for the multi-select) the .multi-anchor collapse together +// when a filter doesn't apply for the current type. +function toggleFilterPair(controlID: string, show: boolean) { + const ctrl = document.getElementById(controlID); + const group = ctrl?.closest(".filter-group"); + if (group) group.style.display = show ? "" : "none"; } function syncURLParams() { @@ -974,6 +968,11 @@ document.addEventListener("DOMContentLoaded", async () => { applyTypeVisibility(); applyView(); onLangChange(() => { + // The static [data-i18n] options are retranslated by initI18n's + // applyTranslations(), but the project select is rebuilt at runtime + // and its "Alle Projekte" / "Nur persönliche" labels come from t() — + // re-run the populator so they pick up the new locale. + populateProjectFilter(); applyTypeVisibility(); render(); }); diff --git a/frontend/src/events.tsx b/frontend/src/events.tsx index f3ba434..bb562d2 100644 --- a/frontend/src/events.tsx +++ b/frontend/src/events.tsx @@ -169,36 +169,46 @@ export function renderEvents(): string {
- - +
+ + +
- - +
+ + +
- -
- - +
+ + +
diff --git a/frontend/src/styles/global.css b/frontend/src/styles/global.css index 9807287..26afda0 100644 --- a/frontend/src/styles/global.css +++ b/frontend/src/styles/global.css @@ -4228,15 +4228,19 @@ input[type="range"]::-moz-range-thumb { .filter-row { display: flex; - align-items: center; + align-items: flex-end; gap: 0.5rem 1rem; flex-wrap: wrap; } +/* Each filter is a label-above-control cell so the caption sits on top of + its select / button. The whole filter-row stays a horizontal flex-wrap + of these column-cells (t-paliad-117). */ .filter-group { display: flex; - align-items: center; - gap: 0.5rem; + flex-direction: column; + align-items: stretch; + gap: 0.25rem; } .filter-label { @@ -4245,13 +4249,8 @@ input[type="range"]::-moz-range-thumb { } @media (max-width: 480px) { - /* Stack each filter as label-above-select, full width — F-24. */ + /* Single-column stack on narrow viewports — F-24. */ .filter-row { flex-direction: column; align-items: stretch; } - .filter-group { - flex-direction: column; - align-items: stretch; - gap: 0.25rem; - } .filter-group .entity-select { width: 100%; } } @@ -8567,7 +8566,13 @@ dialog.quick-add-sheet::backdrop { color: var(--color-text-muted); } -/* Multi-select filter — trigger button + popover panel */ +/* Multi-select filter — trigger button + popover panel. + The .multi-anchor wraps trigger+panel so the absolutely-positioned panel + lands directly under the trigger button (t-paliad-117). */ +.multi-anchor { + position: relative; + display: inline-flex; +} .multi-trigger { display: inline-flex; align-items: center; @@ -8603,6 +8608,14 @@ dialog.quick-add-sheet::backdrop { gap: 0.5rem; max-height: 28rem; } +/* Scoped anchor: pin the panel under its trigger only when wrapped in + .multi-anchor. Agenda's multi-select renders the panel as a sibling + inside a column flex group and relies on auto-positioning, so leave + that path alone. */ +.multi-anchor > .multi-panel { + top: 100%; + left: 0; +} .multi-panel[hidden] { display: none; } .multi-search-row { display: flex; } .multi-search {