UNPKG

@medalsocial/meda

Version:

Shared Meda UI shell and runtime package.

65 lines (64 loc) 3.72 kB
'use client'; import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; import { Calendar, ChevronLeft, ChevronRight } from 'lucide-react'; import { cn } from '../lib/utils.js'; // "YYYY-MM-DD" in the given tz (or local if undefined). Comparing these // strings is a robust same-day check that doesn't depend on local-time math. function ymdInTz(d, tz) { return new Intl.DateTimeFormat('en-CA', { year: 'numeric', month: '2-digit', day: '2-digit', timeZone: tz, }).format(d); } function isSameDay(a, b, tz) { return ymdInTz(a, tz) === ymdInTz(b, tz); } // Returns the tz offset (in minutes) for `date` in `tz`. Positive = east of // UTC, e.g. Europe/Stockholm CET → +60. Tolerates "GMT", "GMT+05", and // "GMT+05:30" formats from Intl.DateTimeFormat. function tzOffsetMinutes(date, tz) { const parts = new Intl.DateTimeFormat('en-US', { timeZone: tz, timeZoneName: 'longOffset', }).formatToParts(date); const name = parts.find((p) => p.type === 'timeZoneName')?.value ?? 'GMT'; const m = /GMT(?:([+-])(\d{1,2})(?::?(\d{2}))?)?/.exec(name); if (!m?.[1]) return 0; const sign = m[1] === '+' ? 1 : -1; const hours = Number.parseInt(m[2] ?? '0', 10); const mins = Number.parseInt(m[3] ?? '0', 10); return sign * (hours * 60 + mins); } function addDays(d, delta, tz) { if (!tz) { // No tz: step the calendar day in the browser's local timezone (DST-safe // there because setDate honors local DST transitions). const c = new Date(d); c.setDate(c.getDate() + delta); return c; } // tz-aware: advance 24h, then correct for any DST shift between the // original day and the target day so the *wall clock* in `tz` lands on the // same hour:minute on the next/previous calendar day. const offsetBefore = tzOffsetMinutes(d, tz); const candidate = new Date(d.getTime() + delta * 86_400_000); const offsetAfter = tzOffsetMinutes(candidate, tz); return new Date(candidate.getTime() + (offsetBefore - offsetAfter) * 60_000); } function fmtDate(d, tz) { return new Intl.DateTimeFormat('en-US', { day: 'numeric', month: 'short', year: 'numeric', timeZone: tz, }).format(d); } export function DateSwitcher({ value, now, tz, onChange, className }) { const isToday = isSameDay(value, now, tz); const goPrev = () => onChange(addDays(value, -1, tz)); const goNext = () => onChange(addDays(value, 1, tz)); return (_jsxs("div", { className: cn('flex items-center gap-2', className), children: [_jsx("button", { type: "button", "aria-label": "Previous day", onClick: goPrev, className: "inline-flex size-7 items-center justify-center rounded-md text-muted-foreground hover:bg-muted hover:text-foreground", children: _jsx(ChevronLeft, { className: "size-4" }) }), _jsxs("div", { className: "flex flex-1 items-center justify-center gap-1.5 rounded-md border border-border bg-card px-3 py-1.5 text-sm", children: [_jsx(Calendar, { className: "size-3.5 text-muted-foreground" }), _jsx("span", { className: "font-medium", children: fmtDate(value, tz) }), isToday && (_jsx("span", { className: "ml-1 rounded-full bg-success/15 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wider text-success-800", children: "today" }))] }), _jsx("button", { type: "button", "aria-label": "Next day", disabled: isToday, onClick: goNext, className: "inline-flex size-7 items-center justify-center rounded-md text-muted-foreground hover:bg-muted hover:text-foreground disabled:cursor-not-allowed disabled:opacity-30 disabled:hover:bg-transparent", children: _jsx(ChevronRight, { className: "size-4" }) })] })); }