独家
Dateline <mv-dateline>
感知时区的时间点或截止时间,让跨日变化不可能被忽略。
文化出处
八十天环游地球,儒勒·凡尔纳(1872 年,小说)。旅行者以为自己差一天输掉了赌约,直到他意识到一路向东跨过日期变更线时多赚了一天:同一时刻在他所在的地方是另一个日期。在 UI 中,每个时刻都同时为两边写出,并明确标出它在查看者那边落在哪一天,这样就没有人会因为差一天而错过截止时间。
工作原理
感知时区的时间点或截止时间,让跨日变化不可能被忽略。给它一个绝对时刻(datetime,带偏移量,或写成其所属时区的当地时间)和所属时区,它会在一句话中写出两边的时间,并附上明确的跨日标签:“Fri, Oct 2, 5:00 PM PT, that’s Sat, Oct 3, 2:00 AM your time (+1 day)”,使用真正的 <time datetime> 元素。inline 变体可直接嵌入正文;card 变体为每个时区添加一条按绝对时间对齐的 24 小时条带(工作时间、晚上、夜间和周末以阴影区分,每个时区的午夜画在其实际位置,该时刻以一条贯穿所有行的线表示,临近时还有一条表示当前时间的虚线),还有显示谁在工作或在睡觉的团队时区、只在文本变化时才唤醒的实时倒计时、夏令时警告(该时刻前后一周内的时钟调整、与今天不同的时差、不存在或出现两次的当地时间)、查看者时区选择器以及日历辅助功能(可附带提醒的 .ics 下载、Google Calendar 链接、复制)。所有时区计算仅使用 Intl.DateTimeFormat:支持任意 IANA 时区、半小时偏移和夏令时切换,并按区域设置使用 12 或 24 小时制。
| 分类 | 数据展示 |
|---|---|
| 类型 | Web Component(<mv-dateline>) |
| 状态 | 稳定版 |
| 套件 | 诚实的系统状态 |
| 同时安装 | button |
| Keywords | exclusive, culture, time-zone, timezone, date, time, deadline, countdown, world-clock, meeting-planner, dst, daylight-saving, intl, ics, calendar, google-calendar, distributed-team, remote, working-hours, relative-time |
When to use
- A deadline, launch or webinar is set in one time zone and read by people all over the world
- An email, status page or changelog must state a maintenance window without anyone misreading the day
- A distributed team needs to see who is working, off or asleep at a proposed meeting time
- A recurring meeting falls in the weeks when regions switch daylight saving time on different dates
Avoid when
- The user has to pick a date or time rather than read one → use Date Picker instead
- The countdown guards a signed-in session that must be kept alive → use Hatch instead
- The time is purely local (a store's opening hours for people standing in it): one plain time is clearer
安装
node scripts/add.mjs dateline --out ./src/marvelous使用 Marvelous UI MCP 服务器的 AI 智能体:install_components({ slugs: ["dateline"], target_dir: "<absolute path>/src/marvelous", framework: "react" })。
复制的文件(含依赖):tokens/tokens.css, core/base.css, components/button/button.css, core/dom.js, core/element.js, core/observe.js, components/dateline/dateline.js, components/dateline/dateline.css。
用法
快速开始,最小可运行标记:
<p>Submissions close <mv-dateline datetime="2026-10-02T17:00" zone="America/Los_Angeles"></mv-dateline>.</p>标准标记,可在此基础上通过属性、data-* 和 CSS 变量进行定制:
<div id="dl-demo" style="width:min(100%,60rem);margin-inline:auto">
<style>
#dl-demo { display:grid; grid-template-columns:minmax(0,1fr); gap:1.25rem; align-content:start }
#dl-demo .dl-bar { display:flex; align-items:center; justify-content:space-between; gap:.75rem 1.25rem; flex-wrap:wrap; padding:.75rem 1rem; border:1px solid var(--mv-border); border-radius:var(--mv-radius-xl); background:var(--mv-bg-subtle) }
#dl-demo .dl-view { display:flex; align-items:center; gap:.5rem .75rem; flex-wrap:wrap; min-width:0; max-width:100% }
#dl-demo .dl-scroll { max-width:100%; overflow-x:auto; scrollbar-width:none }
#dl-demo .dl-k { color:var(--mv-fg-muted); font-size:.8125rem; font-weight:500 }
#dl-demo .dl-tools { display:flex; align-items:center; gap:1rem; flex-wrap:wrap }
#dl-demo .dl-grid { display:grid; grid-template-columns:minmax(0,1.1fr) minmax(0,1fr); gap:1.25rem; align-items:start }
#dl-demo .dl-prose { display:grid; gap:.875rem; padding:1.25rem; border:1px solid var(--mv-border); border-radius:var(--mv-radius-xl); background:var(--mv-surface); box-shadow:var(--mv-shadow-sm) }
#dl-demo .dl-prose h4 { margin:0; font-size:.8125rem; font-weight:500; color:var(--mv-fg-muted) }
#dl-demo .dl-prose p { margin:0; font-size:.9375rem; line-height:1.65 }
#dl-demo .dl-prose .dl-from { display:block; margin-bottom:.125rem; color:var(--mv-fg-subtle); font-size:.75rem }
#dl-demo .dl-sep { height:1px; background:var(--mv-border) }
#dl-demo .dl-log { display:flex; align-items:center; gap:.5rem; min-height:1.5rem; color:var(--mv-fg-muted); font-size:.75rem }
@media (max-width:52rem) { #dl-demo .dl-grid { grid-template-columns:minmax(0,1fr) } }
</style>
<div class="dl-bar">
<div class="dl-view">
<span class="dl-k" id="dl-view-label">Preview as a viewer in</span>
<div class="dl-scroll"><mv-segmented id="dl-viewer" aria-labelledby="dl-view-label" value="">
<button value="">This device</button>
<button value="Europe/Madrid">Madrid</button>
<button value="America/New_York">New York</button>
<button value="Asia/Singapore">Singapore</button>
<button value="Pacific/Auckland">Auckland</button>
</mv-segmented></div>
</div>
<div class="dl-tools">
<label class="mv-choice">
<input type="checkbox" role="switch" class="mv-switch" id="dl-24">
<span class="mv-choice-text"><span class="mv-choice-title">24-hour clock</span></span>
</label>
<button type="button" class="mv-button" data-variant="outline" data-size="sm" id="dl-soon">Deadline in 15 s</button>
</div>
</div>
<!-- A global deadline set in Pacific time, with the team's zones -->
<mv-dateline id="dl-main" variant="card" data-dl-roll
label="Global Hackathon · submissions close"
datetime="2026-10-02T17:00" zone="America/Los_Angeles"
zones="Europe/Paris Asia/Kolkata=Bengaluru Asia/Tokyo"
countdown countdown-text="Closes in {time}" ended-text="Closed {time} ago"
actions="google ics copy" picker reminder="2h"
summary="Global Hackathon: submissions close"
details="Upload your repository link and a 3-minute video on the submission portal."></mv-dateline>
<div class="dl-grid">
<!-- A recurring meeting in the weeks when Europe and the US switch clocks on different dates -->
<mv-dateline id="dl-sync" variant="card"
label="Weekly product sync"
datetime="2026-10-26T09:00" zone="America/New_York"
zones="Europe/London Asia/Singapore"
countdown countdown-text="Starts in {time}" ended-text="Started {time} ago"
duration="45m" actions="ics" location="Video call"></mv-dateline>
<div class="dl-prose">
<h4>In running text</h4>
<p><span class="dl-from">Status page · Planned maintenance</span>
The EU and US clusters go read-only on
<mv-dateline id="dl-maint" data-dl-roll datetime="2026-10-04T23:00Z" zone="UTC" countdown></mv-dateline>
for about 40 minutes.</p>
<div class="dl-sep"></div>
<p><span class="dl-from">Email · Tarnwick Conference 2027</span>
Early-bird registration opens
<mv-dateline id="dl-reg" data-dl-roll datetime="2026-10-01T09:00" zone="Pacific/Auckland" zone-label="Auckland"></mv-dateline>,
with 300 seats at $149.</p>
<div class="dl-sep"></div>
<p class="dl-log" id="dl-log" aria-live="polite"><span class="mv-badge" data-variant="secondary" data-shape="pill" id="dl-log-badge">Events</span><span id="dl-log-text">Change the viewer zone or use the picker.</span></p>
</div>
</div>
<script type="module">
const root = document.getElementById("dl-demo");
await customElements.whenDefined("mv-dateline");
const all = [...root.querySelectorAll("mv-dateline")];
const $ = (id) => document.getElementById(id);
const log = (text, variant = "secondary", badge = "Event") => {
$("dl-log-text").textContent = text;
$("dl-log-badge").textContent = badge;
$("dl-log-badge").dataset.variant = variant;
};
// Keep the demo in the future: roll wall-time dates forward by whole weeks.
const pad = (n) => String(n).padStart(2, "0");
for (const el of root.querySelectorAll("[data-dl-roll]")) {
let guard = 0;
while (el.moment && el.moment.getTime() < Date.now() + 36e5 && guard++ < 200) {
const [date, rest] = el.datetime.split("T");
const d = new Date(`${date}T00:00:00Z`);
d.setUTCDate(d.getUTCDate() + 7);
el.datetime = `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())}T${rest}`;
}
}
// The sync falls on the Monday after Europe's clock change (last Sunday of October).
const sync = $("dl-sync");
const europeGapMonday = (y) => {
const d = new Date(Date.UTC(y, 9, 31));
d.setUTCDate(31 - d.getUTCDay() + 1);
return `${y}-10-${pad(d.getUTCDate())}T09:00`;
};
let y = new Date().getUTCFullYear();
sync.datetime = europeGapMonday(y);
if (sync.moment.getTime() < Date.now()) sync.datetime = europeGapMonday(y + 1);
const viewer = $("dl-viewer");
const setViewer = (zone) => { for (const el of all) el.viewerZone = zone || null; };
viewer.addEventListener("mv-change", (e) => setViewer(e.detail.value));
$("dl-24").addEventListener("change", (e) => { for (const el of all) el.clock = e.target.checked ? "24" : null; });
root.addEventListener("mv-zone-change", (e) => {
if (e.defaultPrevented) return;
if (e.detail.source !== "user") { log(`Device zone changed to ${e.detail.zone}.`, "info", "Zone"); return; }
// The picker of one card drives the whole page.
setViewer(e.detail.automatic ? "" : e.detail.zone);
viewer.value = [...viewer.querySelectorAll("button")].some((b) => b.value === (e.detail.automatic ? "" : e.detail.zone)) ? (e.detail.automatic ? "" : e.detail.zone) : viewer.value;
log(`Showing times in ${e.detail.zone.replace(/_/g, " ")}.`, "info", "Zone");
});
root.addEventListener("mv-calendar", (e) => {
if (e.detail.kind === "google") { e.preventDefault(); log("Google Calendar link ready (opening is disabled in this demo).", "info", "Calendar"); }
else log(`.ics file downloaded${e.target.reminder ? `, with a reminder ${e.target.reminder} before` : ""}.`, "success", "Calendar");
});
root.addEventListener("mv-copy", (e) => log(`Copied: “${e.detail.text}”`, "success", "Copy"));
const main = $("dl-main");
const original = main.datetime;
let restore = 0;
main.addEventListener("mv-due", () => {
log("Submissions are closed: the app can lock the form now.", "warning", "mv-due");
clearTimeout(restore);
restore = setTimeout(() => { if (main.isConnected) main.datetime = original; }, 6000);
});
$("dl-soon").addEventListener("click", () => {
clearTimeout(restore);
main.moment = Date.now() + 15_000;
log("The deadline is now 15 seconds away: the countdown ticks every second in the last hour.", "info", "Countdown");
});
</script>
</div>API
Attributes
| Name | 类型 | Default | Description |
|---|---|---|---|
datetime | ISO 8601 string | The moment. With an offset (Z, +02:00) it is absolute; without one (2026-10-02T17:00) it is read as a wall time in zone, which is what people usually mean (“5 PM Pacific”), DST included. A date alone means midnight. A wall time that does not exist (spring forward) moves forward by the gap, one that happens twice (fall back) takes the first occurrence; both are explained in a note. | |
zone | IANA zone | (the viewer's zone) | Home zone of the moment: the organizer's, the store's, the server's (America/Los_Angeles, UTC…). Unknown zones fall back to the viewer's zone with a console warning. |
zone-label | string | (city of the zone) | Name of the home zone in the rows and notes (“HQ”, “San Francisco”). The sentence uses the short generic abbreviation when the locale has one (PT, ET, UTC), otherwise “<label> time”. |
zones | string | Extra team zones for the card, space- or comma-separated, each optionally followed by =Label: "Europe/Paris Asia/Kolkata=Bengaluru" (use commas when labels contain spaces). Duplicates of the viewer or home zone are skipped. The zones property also accepts an array of strings or { zone, label, workHours }. | |
viewer-zone | IANA zone | (device zone) | Overrides the viewer's zone (a profile setting, a travel preview). Without it the device zone is used and re-checked when the tab regains focus or visibility (mv-zone-change with source "system"). |
variant | inline | card | inline | inline: the sentence and its chips, to sit inside a paragraph. card: label, countdown, the two-line sentence, the zone strips, notes and the footer. |
locale | BCP 47 | en-US | Language and formats of dates, times, units and lists. |
clock | 12 | 24 | (from locale) | Forces a 12-hour or 24-hour clock. |
label | string | What the moment is (“Submissions close”): card heading (and accessible name of the card group), leading words of the inline sentence, and the calendar title unless summary is set. | |
countdown | boolean | Shows a live relative time (“in 2 days 4 hr”, “12 min 5 sec”, “3 hr ago”). Updates every second only in the last hour, otherwise once a minute, and pauses off screen or in a hidden tab. | |
countdown-text / ended-text | string | in {time} / {time} ago | Countdown templates before and after the moment: "Closes in {time}", "Closed {time} ago". |
soon | time ("24h", "90m", "2d") | 24h | Under this remaining time data-state becomes "soon" and the countdown turns amber. |
work-hours / sleep-hours | "H[:MM]-H[:MM]" | 9-18 / 22-7 | Local working and sleeping hours used for every zone's status and strip shading (sleep may wrap midnight). Saturdays and Sundays count as weekend. |
span | number (hours, 12-72) | 24 | Width of the strips, centered on the moment. |
dst-window | number (days) | 7 | A clock change in any shown zone within this many days of the moment adds a note (0 disables it). The “time difference differs from today” and wall-time notes are always on. |
actions | "google ics copy" (any subset) | Card footer buttons: a prefilled Google Calendar link, an .ics download, and Copy (the plain sentence). | |
picker | boolean | Card footer select to show the times in another zone (every IANA zone, grouped by region, with its offset; first option = automatic). Emits a cancelable mv-zone-change, then sets viewer-zone. | |
summary / details / location | string | Calendar event title (default: label), description (the home time is appended) and location. | |
duration | time | 0 | Event length for the calendar helpers ("45m", "1h30m"); 0 for a deadline. |
reminder | time | Adds a VALARM this long before the moment to the .ics file ("2h"). | |
data-state | upcoming | soon | past | invalid | Set by the component (styleable). | |
data-shift | "-1" | "0" | "+1" … | Set by the component: calendar days between the viewer's date and the home date. |
Properties
| Name | 类型 | Description |
|---|---|---|
moment | Date | null | The absolute moment. Setting a Date or epoch ms writes an ISO UTC datetime. |
zones | string | Array<string | { zone, label?, workHours? }> | Team zones (see the attribute). |
dayShift | number | null | Viewer date minus home date, in days (read-only). |
text | string | The full sentence, exactly as announced and copied (read-only). |
notes | string[] | Daylight-saving and wall-time warnings currently computed, also for the inline variant which does not display them (read-only). |
snapshot | Array<{ zone, label, roles, local, iso, offset, status, shift }> | Every shown zone at the moment: local time text, ISO with offset, UTC offset in minutes, status (working | early | evening | asleep | weekend), day shift vs home (read-only). |
currentViewerZone | string | Effective viewer zone (read-only). |
strings | Partial<Record<string, string>> | Overrides for every visible text (keys: that, yourTime, alsoYourTime, sameDay, dayOne, dayOther, future, past, now, you, home, working, early, evening, asleep, weekend, vsHome, zonesTitle, notesTitle, gapChange, relAhead, relBehind, relSame, todayAhead, todayBehind, todaySame, forward, back, forwardMany, backMany, transition, before, after, lessThanDay, wallGap, wallTwice, pickerLabel, auto, google, ics, copy, copied, copyFailed, announceCopied, announceCopyFailed, homeTime, invalid, event). English defaults. |
Methods
| Name | Description |
|---|---|
toICS() | Returns an RFC 5545 calendar file (UTC times, stable UID, folded lines, optional VALARM). |
googleCalendarUrl() | Returns a prefilled Google Calendar event link. |
copy(format?) | Copies "text" (default, the sentence), "iso", "ics" or "google". Emits a cancelable mv-copy first. Resolves true on success. |
refresh() | Recomputes and re-renders now (e.g. after changing strings in place). |
Events
| Name | Description |
|---|---|
mv-zone-change | The viewer's zone changed. detail: { zone, previous, source: "user" | "system", automatic }. Cancelable when it comes from the picker (the select reverts); source "system" means the device zone changed (travel, OS setting) and viewer-zone is not set. |
mv-due | The moment was reached while the component was live. detail: { moment, late } (late: detected after the tab came back). |
mv-calendar | Cancelable, before the Google Calendar link opens or the .ics downloads. detail: { kind: "google" | "ics", url, ics }. preventDefault() to run your own flow. |
mv-copy | Cancelable, before copying. detail: { format, text }. |
Content structure
| Name | Description |
|---|---|
(content) | Optional fallback text (for no-JS or server rendering), replaced once the element is defined. |
CSS classes
| Name | Description |
|---|---|
mv-dateline-sentence | The sentence: .mv-dateline-lead (inline label), <time>.mv-dateline-home, .mv-dateline-join (.mv-dateline-dash + “that’s”), <time>.mv-dateline-local, .mv-dateline-you, .mv-dateline-shift[data-shift=ahead|behind|same]; .mv-dateline-paren holds visually hidden punctuation. |
mv-dateline-head / -label / -countdown | Card heading and the role="timer" countdown chip. |
mv-dateline-zones / -list / -zone | Zone rows (li[data-role=you|home|team][data-status]): -zone-info (-zone-name, -city, -tag, -zone-time, -status, -dot, -rel) and the aria-hidden -track (-bar with -seg[data-band=work|off|night|weekend], -midnight, -now, -mark; -ticks with -tick[data-day]). |
mv-dateline-notes | role="note" box with the daylight-saving warnings (-notes-title, -notes-list). |
mv-dateline-foot / -picker / -select / -actions / -action | Footer with the zone picker and the calendar buttons (data-action=google|ics|copy, data-copied after a copy). |
CSS variables
| Name | Default | Description |
|---|---|---|
--mv-dateline-shift | var(--mv-warning) | Tint of the day-shift chips and of the shifted local time. |
--mv-dateline-marker | var(--mv-accent) | The moment line and dot across the strips. |
--mv-dateline-work | accent 30% on surface | Working-hours shading. |
--mv-dateline-off | var(--mv-bg-emphasis) | Off-hours shading (weekends add a fine hatch). |
--mv-dateline-night | darker muted tone | Sleeping-hours shading. |
--mv-dateline-info-width | 12.5rem | Width of the zone info column beside the strips (stacked under 34rem of card width). |
Accessibility
The sentence is the accessible text: real <time datetime> elements with the home zone's ISO offset and the viewer's, and the day shift spelled out as “(+1 day)” (parentheses visually hidden behind the chip, “−” read as minus), so screen readers get the same statement sighted users see and Copy puts on the clipboard. In the card, the strips are aria-hidden with pointer-events untouched; their text equivalent is the labelled list beside them (“At that moment”), one item per zone with its name, You / Home tag, local time, day shift and status (“Asleep · +9h vs home”). The card is a group named by its label. The countdown is a role="timer" element, whose implicit aria-live is off: it is read on demand and never announces each second; the component never announces time passing. Daylight-saving warnings are a role="note" block with a heading. The picker is a native select with a visible label, the actions are a real link (Google Calendar, new tab), a download link and a button; a copy is confirmed by a polite live region and a label change, and failures say so. Status is never color-only (dot plus words), shifts are text, and the chip, tags and strips keep borders or system colors in forced-colors mode. No animation is used, so reduced motion needs nothing. Everything visible is overridable through strings for other languages.