Merge: t-paliad-278 — date-range picker 3-column layout Past/NOW/Future (m/paliad#110)

This commit is contained in:
mAi
2026-05-25 16:46:59 +02:00
4 changed files with 120 additions and 133 deletions

View File

@@ -191,25 +191,37 @@ export function mountDateRangePicker(opts: MountOpts): PickerHandle {
function renderPanel(): void {
panel.replaceChildren();
// Three groups in a single row: past fan / ALLES centre / next fan.
const row = document.createElement("div");
row.className = "date-range-row";
// Three vertical columns: Past (closest→farthest top→bottom),
// NOW (Heute + Alles), Future (closest→farthest). The grid
// visualises time as space around NOW — each column's top is
// closest to the current moment, bottom is furthest away.
const grid = document.createElement("div");
grid.className = "date-range-grid";
const pastGroup = renderFan(
PAST_HORIZONS.filter((h) => presets.includes(h)),
// Past column: PAST_HORIZONS registry is outermost→innermost
// (past_all → past_1d); reverse for closeness-to-NOW ordering
// (past_1d at top, past_all at bottom).
const pastCol = renderColumn(
"past",
t("date_range.fan.past.label"),
[...PAST_HORIZONS].reverse().filter((h) => presets.includes(h)),
);
const centerGroup = renderCenter();
const nextGroup = renderFan(
NEXT_HORIZONS.filter((h) => presets.includes(h)),
"next",
const nowCol = renderNowColumn();
// Future column: NEXT_HORIZONS registry is already in closeness
// order (next_1d → next_all). next_1d moves to the NOW column as
// "Heute" (semantically just-today, single-day window), so the
// future column skips it.
const futureCol = renderColumn(
"future",
t("date_range.fan.future.label"),
NEXT_HORIZONS.filter((h) => h !== "next_1d" && presets.includes(h)),
);
if (pastGroup) row.appendChild(pastGroup);
if (centerGroup) row.appendChild(centerGroup);
if (nextGroup) row.appendChild(nextGroup);
if (pastCol) grid.appendChild(pastCol);
if (nowCol) grid.appendChild(nowCol);
if (futureCol) grid.appendChild(futureCol);
panel.appendChild(row);
panel.appendChild(grid);
// Custom-range section ("Anpassen"). Toggle button + collapsible
// date-pair editor below.
@@ -218,49 +230,57 @@ export function mountDateRangePicker(opts: MountOpts): PickerHandle {
}
}
function renderFan(horizons: readonly TimeHorizon[], side: "past" | "next"): HTMLElement | null {
function renderColumn(
side: "past" | "future",
heading: string,
horizons: readonly TimeHorizon[],
): HTMLElement | null {
if (horizons.length === 0) return null;
const group = document.createElement("div");
group.className = `date-range-fan date-range-fan--${side}`;
group.setAttribute("role", "group");
group.setAttribute("aria-label", side === "past"
? t("date_range.fan.past.label")
: t("date_range.fan.future.label"));
const col = document.createElement("div");
col.className = `date-range-col date-range-col--${side}`;
col.setAttribute("role", "group");
col.setAttribute("aria-label", heading);
const head = document.createElement("div");
head.className = "date-range-col-heading";
head.textContent = heading;
col.appendChild(head);
for (const h of horizons) {
group.appendChild(makeChip(h));
col.appendChild(makeChip(h));
}
return group;
return col;
}
function renderCenter(): HTMLElement | null {
if (!presets.includes("any")) return null;
const wrap = document.createElement("div");
wrap.className = "date-range-center";
const btn = document.createElement("button");
btn.type = "button";
btn.className = "date-range-center-btn";
if (value.horizon === "any" || value.horizon === "all") {
btn.classList.add("date-range-center-btn--active");
}
btn.setAttribute("aria-pressed", String(value.horizon === "any" || value.horizon === "all"));
btn.dataset.testid = `${opts.surface}.date-range-chip.any`;
function renderNowColumn(): HTMLElement | null {
const showHeute = presets.includes("next_1d");
const showAlles = presets.includes("any");
if (!showHeute && !showAlles) return null;
const glyph = document.createElement("span");
glyph.className = "date-range-center-glyph";
const col = document.createElement("div");
col.className = "date-range-col date-range-col--now";
col.setAttribute("role", "group");
col.setAttribute("aria-label", t("date_range.center.label"));
const glyph = document.createElement("div");
glyph.className = "date-range-col-heading date-range-col-heading--glyph";
glyph.setAttribute("aria-hidden", "true");
glyph.textContent = "⌖"; // ⌖ POSITION INDICATOR
const label = document.createElement("span");
label.className = "date-range-center-label";
label.textContent = t("date_range.center.label");
btn.appendChild(glyph);
btn.appendChild(label);
col.appendChild(glyph);
btn.addEventListener("click", () => {
commit({ horizon: "any" }, /*closeAfter*/ true);
});
wrap.appendChild(btn);
return wrap;
if (showHeute) col.appendChild(makeChip("next_1d"));
if (showAlles) {
const allesChip = makeChip("any");
// Legacy "all" horizon also lights up Alles for back-compat
// with saved Custom Views that store the bidirectional-unbounded
// value (Q26 — parser preserves it, picker surfaces it here).
if (value.horizon === "all") {
allesChip.classList.add("agenda-chip-active");
allesChip.setAttribute("aria-pressed", "true");
}
col.appendChild(allesChip);
}
return col;
}
function makeChip(h: TimeHorizon): HTMLButtonElement {

View File

@@ -73,13 +73,16 @@ export function renderAxis(axis: AxisKey, ctx: AxisCtx, opts?: RenderAxisOpts):
type TimeHorizonValue = NonNullable<BarState["time"]>["horizon"];
// Default chip set when the surface doesn't override. Matches the
// forward-leaning bias of the legacy filter-bar default (the universal
// substrate is more often used for "what's coming up" than "what just
// happened") but now covers the full symmetric fan plus past_30d for
// quick recent-history lookups.
// Default chip set when the surface doesn't override. Mirrors m's
// 3-column picker spec (t-paliad-278): symmetric 7d/30d/90d/all fan
// per side, plus Heute (next_1d) + Alles (any) in the centre column,
// plus Anpassen. Surfaces with a tighter scope (project history is
// past-only) keep overriding via `timePresets`.
const DEFAULT_TIME_PRESETS: TimeHorizonValue[] = [
"past_30d", "past_7d", "any", "next_7d", "next_30d", "next_90d", "custom",
"past_7d", "past_30d", "past_90d", "past_all",
"next_1d", "any",
"next_7d", "next_30d", "next_90d", "next_all",
"custom",
];
function renderTimeAxis(ctx: AxisCtx, presetOverride?: TimeHorizonValue[]): HTMLElement {

View File

@@ -3062,7 +3062,7 @@ const translations: Record<Lang, Record<string, string>> = {
// /admin/audit-log to the same component.
"date_range.button.label": "Zeitraum",
"date_range.button.label.custom_range": "Von {from} bis {to}",
"date_range.horizon.next_1d": "Morgen",
"date_range.horizon.next_1d": "Heute",
"date_range.horizon.next_7d": "Nächste 7 Tage",
"date_range.horizon.next_14d": "Nächste 14 Tage",
"date_range.horizon.next_30d": "Nächste 30 Tage",
@@ -6110,7 +6110,7 @@ const translations: Record<Lang, Record<string, string>> = {
// Date-range picker (t-paliad-248). See DE block above for details.
"date_range.button.label": "Time range",
"date_range.button.label.custom_range": "From {from} to {to}",
"date_range.horizon.next_1d": "Tomorrow",
"date_range.horizon.next_1d": "Today",
"date_range.horizon.next_7d": "Next 7 days",
"date_range.horizon.next_14d": "Next 14 days",
"date_range.horizon.next_30d": "Next 30 days",

View File

@@ -17760,9 +17760,10 @@ dialog.quick-add-sheet::backdrop {
}
.date-range-panel {
/* Inherits .multi-panel positioning + border + shadow. Widen it so
the symmetric fan + the custom editor have room to breathe. */
width: 32rem;
/* Inherits .multi-panel positioning + border + shadow. Sized so the
3-column grid holds the widest chip text ("Ganze Vergangenheit")
without wrapping while staying within the viewport on tablets. */
width: 34rem;
max-width: calc(100vw - 1rem);
top: 100%;
left: 0;
@@ -17770,88 +17771,54 @@ dialog.quick-add-sheet::backdrop {
gap: 0.75rem;
}
.date-range-row {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
align-items: stretch;
.date-range-grid {
/* Past / NOW / Future as three equal vertical columns. Each column
is a top-aligned chip stack so closeness-to-NOW (closest at top,
farthest at bottom) reads spatially. */
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 0.75rem;
align-items: start;
}
.date-range-fan {
.date-range-col {
display: flex;
flex-wrap: wrap;
gap: 0.3rem;
align-content: flex-start;
flex: 1 1 12rem;
flex-direction: column;
gap: 0.35rem;
min-width: 0;
}
.date-range-fan--past {
/* Past fan: outermost chip (Ganze Vergangenheit) leftmost. */
justify-content: flex-end;
.date-range-col--now {
align-items: stretch;
}
.date-range-fan--next {
/* Future fan: innermost chip (Morgen / next_1d) leftmost. */
justify-content: flex-start;
}
.date-range-center {
display: flex;
align-items: center;
justify-content: center;
flex: 0 0 auto;
padding: 0 0.25rem;
}
.date-range-center-btn {
appearance: none;
display: inline-flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.1rem;
background: var(--color-surface-muted);
border: 1px solid var(--color-border);
border-radius: 0.6rem;
min-width: 4.5rem;
padding: 0.55rem 0.75rem;
font-size: 0.75rem;
font-weight: 600;
color: var(--color-text);
cursor: pointer;
transition: background 0.15s, border-color 0.15s, color 0.15s;
}
.date-range-center-btn:hover {
background: var(--color-overlay-subtle);
border-color: var(--color-accent-light);
}
.date-range-center-btn:focus-visible {
outline: 2px solid var(--color-accent);
outline-offset: 2px;
}
.date-range-center-btn--active {
background: var(--color-accent);
border-color: var(--color-accent);
color: var(--color-accent-dark);
}
.date-range-center-glyph {
font-size: 1.4rem;
line-height: 1;
}
.date-range-center-label {
.date-range-col-heading {
font-size: 0.7rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--color-text-muted, #71717a);
text-align: center;
padding-bottom: 0.15rem;
}
.date-range-col-heading--glyph {
font-size: 1.3rem;
line-height: 1;
letter-spacing: 0;
text-transform: none;
color: var(--color-text-muted, #71717a);
}
.date-range-chip {
/* .agenda-chip provides bg/border/radius/typography; this modifier
only tightens horizontal padding so more chips fit per row. */
padding: 0.3rem 0.65rem;
/* .agenda-chip provides bg/border/radius/typography; in the
3-column stack each chip fills its column so the closeness-to-NOW
ordering reads as a single vertical column rather than a ragged
row. */
padding: 0.35rem 0.65rem;
font-size: 0.8rem;
width: 100%;
text-align: center;
}
.date-range-chip--custom {
@@ -17938,17 +17905,14 @@ dialog.quick-add-sheet::backdrop {
color: var(--status-red-fg, #b91c1c);
}
/* Mobile: stack past / centre / next vertically so each fan gets
the full popover width. */
/* Mobile: stack the 3 columns vertically (one column per row),
preserving the closeness-to-NOW sort within each column. */
@media (max-width: 540px) {
.date-range-panel {
width: calc(100vw - 1rem);
}
.date-range-row {
flex-direction: column;
}
.date-range-fan--past,
.date-range-fan--next {
justify-content: flex-start;
.date-range-grid {
grid-template-columns: 1fr;
gap: 0.5rem;
}
}