Files
HealthLog/src/components/day/day-url.ts
T
Marc-André Bombeck d8fb922228 feat(shell): give each docked panel a fixed strip at the right edge
From 1280 px the day and, on /coach, the conversations each keep a
narrow strip at the right edge, open or not. A click on the strip opens
its panel left of the strips and a second click closes it; the strip
never moves. Strips start below the top bar's band, side by side,
conversations first. Below 1600 px one panel is open at a time.

The day slides like the conversations (width, 200 ms, none under reduced
motion), stays open through a page change instead of folding and
reopening, and does not slide in on arrival. Its content loads when a day
first opens, so the scores, sections and calendar leave the shell's first
load: every watched route falls back under its unchanged ceiling. The
aggregate cap is re-measured for the chunk double count this causes.
2026-10-09 18:22:11 +02:00

124 lines
4.2 KiB
TypeScript

/**
* The day layer's address: `?day=YYYY-MM-DD` on whatever page is open.
*
* Pure helpers, no React, so the rules the layer lives by (which dates open,
* how the URL is spelled, what a step to the neighbouring day lands on) can be
* pinned without mounting anything.
*
* A day opens only when it names a real calendar date that is not in the
* future of the record's own zone. A future day has no content to show; a
* link to one would be a dead end, so such a parameter is dropped quietly,
* the same way `?add=` is on the measurements page.
*/
import { DAY_QUERY_PARAM, type DateKey } from "@/lib/day/contract";
import { dateOnlyKey, isCalendarDateKey } from "@/lib/tz/date-only";
import { getDateTimeFormat } from "@/lib/tz/intl-cache";
/**
* The history-state marker of an entry the layer pushed itself. Closing such
* an entry goes back one step; closing a deep link only edits the URL, so the
* person stays on the page they arrived at.
*/
export const DAY_HISTORY_KEY = "__healthlogDayLayer";
/** Today's calendar date in `timeZone`, `YYYY-MM-DD`. */
export function todayKeyInZone(timeZone: string, now = new Date()): DateKey {
// en-CA spells a date as YYYY-MM-DD.
return getDateTimeFormat("en-CA", timeZone, {
year: "numeric",
month: "2-digit",
day: "2-digit",
}).format(now);
}
/** The calendar date of an instant in `timeZone`. */
export function dateKeyOfInstant(
instant: Date | string | number,
timeZone: string,
): DateKey {
return todayKeyInZone(timeZone, new Date(instant));
}
/** True when `key` is a real date on or before `todayKey`. */
export function isOpenableDay(
key: string | null | undefined,
todayKey: DateKey,
): key is DateKey {
return typeof key === "string" && isCalendarDateKey(key) && key <= todayKey;
}
/** The parameter's day, or null when it names nothing the layer can open. */
export function parseDayParam(
raw: string | null,
todayKey: DateKey,
): DateKey | null {
return isOpenableDay(raw, todayKey) ? raw : null;
}
/**
* The day the docked day's strip holds: the open day, else the remembered
* one while the layer would still open it, else today. The strip always
* names a day, so it always has something to open.
*/
export function stripDayOf(
open: DateKey | null,
remembered: string | null,
todayKey: DateKey,
): DateKey {
return open ?? parseDayParam(remembered, todayKey) ?? todayKey;
}
/** `key` moved by `delta` calendar days. */
export function shiftDateKey(key: DateKey, delta: number): DateKey {
const anchor = new Date(`${key}T12:00:00.000Z`);
anchor.setUTCDate(anchor.getUTCDate() + delta);
return dateOnlyKey(anchor);
}
/** Calendar days from `from` to `to` (negative when `to` is earlier). */
export function daysBetween(from: DateKey, to: DateKey): number {
const a = Date.parse(`${from}T12:00:00.000Z`);
const b = Date.parse(`${to}T12:00:00.000Z`);
return Math.round((b - a) / 86_400_000);
}
/** The current location with the day set, or removed when `key` is null. */
export function withDayHref(
pathname: string,
search: string,
key: DateKey | null,
): string {
const next = new URLSearchParams(search);
if (key === null) next.delete(DAY_QUERY_PARAM);
else next.set(DAY_QUERY_PARAM, key);
const query = next.toString();
return query ? `${pathname}?${query}` : pathname;
}
/**
* The link a day carries off its own page: the dashboard with the day open
* over it. Closing the layer there lands on today, which is what a link from
* outside the page (a Coach chip, a notification) should do.
*/
export function externalDayHref(key: DateKey): string {
return `/?${DAY_QUERY_PARAM}=${key}`;
}
/**
* The history state the layer writes. Only application state: Next copies its
* router fields onto the object itself, and copying `history.state` here would
* carry them over and make Next treat the write as its own.
*/
export function dayHistoryState(key: DateKey): Record<string, unknown> {
return { [DAY_HISTORY_KEY]: key };
}
/** True when the current history entry was pushed by the layer. */
export function historyEntryOwnedByLayer(state: unknown): boolean {
return (
state !== null &&
typeof state === "object" &&
typeof (state as Record<string, unknown>)[DAY_HISTORY_KEY] === "string"
);
}