Merge: t-paliad-117 — /events filter polish (i18n leak + label position + panel anchoring)
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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<HTMLLabelElement>(`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<HTMLElement>(".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();
|
||||
});
|
||||
|
||||
@@ -169,6 +169,7 @@ export function renderEvents(): string {
|
||||
|
||||
<div className="entity-controls">
|
||||
<div className="filter-row">
|
||||
<div className="filter-group">
|
||||
<label className="filter-label" htmlFor="events-filter-status" data-i18n="deadlines.filter.status">Status</label>
|
||||
<select id="events-filter-status" className="entity-select">
|
||||
<option value="all" data-i18n="deadlines.filter.all">Alle (offen & erledigt)</option>
|
||||
@@ -180,17 +181,25 @@ export function renderEvents(): string {
|
||||
<option value="later" data-i18n="deadlines.filter.later">Später</option>
|
||||
<option value="completed" data-i18n="deadlines.filter.completed">Erledigt</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="filter-group" id="events-filter-project-group">
|
||||
<label className="filter-label" htmlFor="events-filter-project" data-i18n="deadlines.filter.akte">Projekt</label>
|
||||
<select id="events-filter-project" className="entity-select">
|
||||
<option value="" data-i18n="deadlines.filter.akte.all">Alle Projekte</option>
|
||||
<option value="__personal__" data-i18n="appointments.filter.akte.personal">Nur persönliche</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="filter-group" id="events-filter-event-type-group">
|
||||
<label className="filter-label" htmlFor="events-filter-event-type" id="events-filter-event-type-label" data-i18n="deadlines.filter.event_type">Typ</label>
|
||||
<div className="multi-anchor">
|
||||
<button type="button" id="events-filter-event-type" className="entity-select multi-trigger" aria-haspopup="listbox" />
|
||||
<div id="events-filter-event-type-panel" className="multi-panel" hidden />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="filter-group" id="events-filter-appointment-type-group">
|
||||
<label className="filter-label" htmlFor="events-filter-appointment-type" id="events-filter-appointment-type-label" data-i18n="appointments.filter.type">Typ</label>
|
||||
<select id="events-filter-appointment-type" className="entity-select">
|
||||
<option value="" data-i18n="appointments.filter.type.all">Alle Typen</option>
|
||||
@@ -201,6 +210,7 @@ export function renderEvents(): string {
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="events-unavailable" className="entity-unavailable" style="display:none">
|
||||
<p data-i18n="events.unavailable">
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user