@aidget-community/countdown 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agents.md +29 -0
- package/assets/screenshot.png +0 -0
- package/config.json +12 -0
- package/index.html +448 -0
- package/package.json +22 -4
- package/style.css +351 -0
- package/README.md +0 -3
package/agents.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# 倒数日
|
|
2
|
+
|
|
3
|
+
## 用途
|
|
4
|
+
|
|
5
|
+
这是一张多事件倒数卡片。包内初始内容是“纪念日 / 元旦”两个真实示例;页面按本地日历日计算,一次显示一个事件,事件多于一个时可在左下角左右切换。
|
|
6
|
+
|
|
7
|
+
## 设置事件
|
|
8
|
+
|
|
9
|
+
- 先读取当前组件和 revision。使用 `aidget_patch` 更新 `events` 区域,保留外层 `data-aidget-region="events"`。
|
|
10
|
+
- 区域内按顺序放置一个或多个 `<article class="event-data" data-event data-event-repeat="...">`;顺序就是切换顺序,至少保留一个事件。
|
|
11
|
+
- 每个事件包含 `h2[data-event-title]`、`time[data-event-date]` 和可选的 `p[data-event-note]`。日期写入 `datetime` 属性并使用有效的 `YYYY-MM-DD`,显示文字由页面生成。
|
|
12
|
+
- 重复周期值只能是 `once`、`weekly`、`monthly` 或 `yearly`,分别表示一次性、每周、每月和每年。周期只用于计算,不显示在卡片上。
|
|
13
|
+
- 只提供事件数据,不要添加计数、按钮、页码、周期标签或目标日期详情;页面会生成这些内容。事件名尽量不超过两行,说明保持在两行以内。
|
|
14
|
+
- 事件多于一个时页面自动显示左右切换,只有一个时隐藏切换控件。
|
|
15
|
+
- 示例区域内写 HTML 时,转义事件名称和说明中的 `&`、`<`、`>`;不要加入脚本、外部资源或事件处理属性。
|
|
16
|
+
- 修改时一次原子 patch 整个 `events` 区域,避免残留旧事件。遇到 revision 冲突时重新读取,勿覆盖用户同时做出的更改。
|
|
17
|
+
|
|
18
|
+
## 日期规则
|
|
19
|
+
|
|
20
|
+
- 日期没有时区,按本地日历日计算;目标日当天显示 0 天。
|
|
21
|
+
- 一次性日期未来显示剩余天数,过去显示已过去天数。
|
|
22
|
+
- 每周以设定日期的星期为锚点;每月以设定日为锚点;每年以设定月日为锚点。重复周期都倒数到下一次出现的日期。
|
|
23
|
+
- 每月遇到较短月份时落到该月最后一天;闰日遇到非闰年时落到 2 月 28 日。
|
|
24
|
+
- 不要猜测或编造用户的事件、日期和说明。日期为空或设置无效时,卡片会显示错误。
|
|
25
|
+
|
|
26
|
+
## 外观与权限
|
|
27
|
+
|
|
28
|
+
- 保留暖白底、钴蓝色卡片、柿橙点缀、前景大号数字和左下角切换控件;不要移动或调整用户窗口。
|
|
29
|
+
- 卡片使用本地日期脚本,只需 `script` 权限。`network` 保持为空;不使用 `storage`、远程字体、图片或依赖。
|
|
Binary file
|
package/config.json
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "countdown",
|
|
3
|
+
"title": "倒数日",
|
|
4
|
+
"minHostVersion": "0.1.1",
|
|
5
|
+
"logo": null,
|
|
6
|
+
"screenshots": ["assets/screenshot.png"],
|
|
7
|
+
"platforms": ["macos", "windows", "linux"],
|
|
8
|
+
"size": { "width": 360, "height": 220 },
|
|
9
|
+
"dragMode": "all",
|
|
10
|
+
"permissions": ["script"],
|
|
11
|
+
"network": []
|
|
12
|
+
}
|
package/index.html
ADDED
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<meta name="aidget-surface" content="none" />
|
|
7
|
+
<title>倒数日</title>
|
|
8
|
+
<link rel="stylesheet" href="./style.css" />
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<main class="card" id="card" data-state="upcoming" aria-label="纪念日,还有 196 天">
|
|
12
|
+
<div class="blue-swatch" aria-hidden="true"></div>
|
|
13
|
+
<div class="orange-swatch" aria-hidden="true"></div>
|
|
14
|
+
|
|
15
|
+
<section class="event-source" data-aidget-region="events" aria-label="倒计时事件" hidden>
|
|
16
|
+
<article class="event-data" data-event data-event-repeat="yearly">
|
|
17
|
+
<h2 data-event-title>纪念日</h2>
|
|
18
|
+
<time data-event-date datetime="2027-05-20"></time>
|
|
19
|
+
<p data-event-note>拍一张新的合照。</p>
|
|
20
|
+
</article>
|
|
21
|
+
<article class="event-data" data-event data-event-repeat="yearly">
|
|
22
|
+
<h2 data-event-title>元旦</h2>
|
|
23
|
+
<time data-event-date datetime="2027-01-01"></time>
|
|
24
|
+
<p data-event-note>去海边,等第一束光。</p>
|
|
25
|
+
</article>
|
|
26
|
+
</section>
|
|
27
|
+
|
|
28
|
+
<section class="stage" data-stage></section>
|
|
29
|
+
|
|
30
|
+
<nav class="event-nav" data-event-nav data-aidget-no-drag aria-label="切换事件" hidden>
|
|
31
|
+
<button class="nav-button" type="button" data-nav="previous" aria-label="上一个倒计时">
|
|
32
|
+
<svg viewBox="0 0 16 16" aria-hidden="true" focusable="false">
|
|
33
|
+
<path d="M10.2 3.2 5.4 8l4.8 4.8"></path>
|
|
34
|
+
</svg>
|
|
35
|
+
</button>
|
|
36
|
+
<div class="event-dots" data-event-dots aria-hidden="true"></div>
|
|
37
|
+
<button class="nav-button" type="button" data-nav="next" aria-label="下一个倒计时">
|
|
38
|
+
<svg viewBox="0 0 16 16" aria-hidden="true" focusable="false">
|
|
39
|
+
<path d="M5.8 3.2 10.6 8l-4.8 4.8"></path>
|
|
40
|
+
</svg>
|
|
41
|
+
</button>
|
|
42
|
+
</nav>
|
|
43
|
+
</main>
|
|
44
|
+
|
|
45
|
+
<template id="event-template">
|
|
46
|
+
<article class="event" data-event-slide>
|
|
47
|
+
<div class="event-copy">
|
|
48
|
+
<h1 class="event-title" data-slide-title></h1>
|
|
49
|
+
<time class="event-date" data-slide-date></time>
|
|
50
|
+
<p class="event-note" data-slide-note></p>
|
|
51
|
+
</div>
|
|
52
|
+
<section class="count-panel" aria-label="倒数">
|
|
53
|
+
<p class="count-context" data-slide-label></p>
|
|
54
|
+
<div class="number-lockup">
|
|
55
|
+
<output
|
|
56
|
+
class="count-number"
|
|
57
|
+
data-slide-count
|
|
58
|
+
aria-live="polite"
|
|
59
|
+
aria-atomic="true"
|
|
60
|
+
></output>
|
|
61
|
+
<span class="count-unit">天</span>
|
|
62
|
+
</div>
|
|
63
|
+
</section>
|
|
64
|
+
</article>
|
|
65
|
+
</template>
|
|
66
|
+
|
|
67
|
+
<script>
|
|
68
|
+
(() => {
|
|
69
|
+
const DAY_MS = 86_400_000;
|
|
70
|
+
const REPEATS = new Set(['once', 'weekly', 'monthly', 'yearly']);
|
|
71
|
+
|
|
72
|
+
function parseDate(value) {
|
|
73
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value ?? '');
|
|
74
|
+
if (!match) return null;
|
|
75
|
+
const parts = {
|
|
76
|
+
year: Number(match[1]),
|
|
77
|
+
month: Number(match[2]),
|
|
78
|
+
day: Number(match[3]),
|
|
79
|
+
};
|
|
80
|
+
const date = new Date(0);
|
|
81
|
+
date.setUTCHours(0, 0, 0, 0);
|
|
82
|
+
date.setUTCFullYear(parts.year, parts.month - 1, parts.day);
|
|
83
|
+
if (
|
|
84
|
+
date.getUTCFullYear() !== parts.year ||
|
|
85
|
+
date.getUTCMonth() !== parts.month - 1 ||
|
|
86
|
+
date.getUTCDate() !== parts.day
|
|
87
|
+
) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
return parts;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function ordinal(parts) {
|
|
94
|
+
const date = new Date(0);
|
|
95
|
+
date.setUTCHours(0, 0, 0, 0);
|
|
96
|
+
date.setUTCFullYear(parts.year, parts.month - 1, parts.day);
|
|
97
|
+
return date.getTime() / DAY_MS;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function dateKey(parts) {
|
|
101
|
+
return `${String(parts.year).padStart(4, '0')}-${String(parts.month).padStart(2, '0')}-${String(parts.day).padStart(2, '0')}`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function compareDates(left, right) {
|
|
105
|
+
return Math.sign(ordinal(left) - ordinal(right));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function addDays(parts, days) {
|
|
109
|
+
const date = new Date((ordinal(parts) + days) * DAY_MS);
|
|
110
|
+
return {
|
|
111
|
+
year: date.getUTCFullYear(),
|
|
112
|
+
month: date.getUTCMonth() + 1,
|
|
113
|
+
day: date.getUTCDate(),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function daysInMonth(year, month) {
|
|
118
|
+
const date = new Date(0);
|
|
119
|
+
date.setUTCHours(0, 0, 0, 0);
|
|
120
|
+
date.setUTCFullYear(year, month, 0);
|
|
121
|
+
return date.getUTCDate();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function dateInMonth(year, month, day) {
|
|
125
|
+
return {
|
|
126
|
+
year,
|
|
127
|
+
month,
|
|
128
|
+
day: Math.min(day, daysInMonth(year, month)),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function nextWeekly(start, today) {
|
|
133
|
+
if (compareDates(today, start) <= 0) return start;
|
|
134
|
+
const elapsed = ordinal(today) - ordinal(start);
|
|
135
|
+
return addDays(start, Math.ceil(elapsed / 7) * 7);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function nextMonthly(start, today) {
|
|
139
|
+
const threshold = compareDates(today, start) < 0 ? start : today;
|
|
140
|
+
let candidate = dateInMonth(threshold.year, threshold.month, start.day);
|
|
141
|
+
if (compareDates(candidate, threshold) < 0) {
|
|
142
|
+
const nextMonth = threshold.month === 12 ? 1 : threshold.month + 1;
|
|
143
|
+
const nextYear = threshold.month === 12 ? threshold.year + 1 : threshold.year;
|
|
144
|
+
candidate = dateInMonth(nextYear, nextMonth, start.day);
|
|
145
|
+
}
|
|
146
|
+
return candidate;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function nextYearly(start, today) {
|
|
150
|
+
const threshold = compareDates(today, start) < 0 ? start : today;
|
|
151
|
+
let candidate = dateInMonth(threshold.year, start.month, start.day);
|
|
152
|
+
if (compareDates(candidate, threshold) < 0) {
|
|
153
|
+
candidate = dateInMonth(threshold.year + 1, start.month, start.day);
|
|
154
|
+
}
|
|
155
|
+
return candidate;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function localDateKey(date = new Date()) {
|
|
159
|
+
return dateKey({
|
|
160
|
+
year: date.getFullYear(),
|
|
161
|
+
month: date.getMonth() + 1,
|
|
162
|
+
day: date.getDate(),
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function calculateCountdown(dateValue, repeat, todayValue = localDateKey()) {
|
|
167
|
+
const start = parseDate(dateValue);
|
|
168
|
+
const today = parseDate(todayValue);
|
|
169
|
+
if (!start) throw new Error('日期必须是有效的 YYYY-MM-DD。');
|
|
170
|
+
if (!today) throw new Error('当前本地日期无效。');
|
|
171
|
+
if (!REPEATS.has(repeat))
|
|
172
|
+
throw new Error('重复周期必须是 once、weekly、monthly 或 yearly。');
|
|
173
|
+
|
|
174
|
+
if (repeat === 'once') {
|
|
175
|
+
const difference = ordinal(start) - ordinal(today);
|
|
176
|
+
return {
|
|
177
|
+
days: Math.abs(difference),
|
|
178
|
+
mode: difference < 0 ? 'elapsed' : difference === 0 ? 'today' : 'upcoming',
|
|
179
|
+
targetDate: dateKey(start),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const next =
|
|
184
|
+
repeat === 'weekly'
|
|
185
|
+
? nextWeekly(start, today)
|
|
186
|
+
: repeat === 'monthly'
|
|
187
|
+
? nextMonthly(start, today)
|
|
188
|
+
: nextYearly(start, today);
|
|
189
|
+
const difference = ordinal(next) - ordinal(today);
|
|
190
|
+
return {
|
|
191
|
+
days: difference,
|
|
192
|
+
mode: difference === 0 ? 'today' : 'upcoming',
|
|
193
|
+
targetDate: dateKey(next),
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function formatDate(value) {
|
|
198
|
+
const parts = parseDate(value);
|
|
199
|
+
if (!parts) throw new Error('日期必须是有效的 YYYY-MM-DD。');
|
|
200
|
+
return `${parts.year}年${parts.month}月${parts.day}日`;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
window.__countdownTestApi = Object.freeze({
|
|
204
|
+
calculateCountdown,
|
|
205
|
+
formatDate,
|
|
206
|
+
parseDate,
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
const doc = window.document;
|
|
210
|
+
const card = doc.querySelector('#card');
|
|
211
|
+
const stage = doc.querySelector('[data-stage]');
|
|
212
|
+
const nav = doc.querySelector('[data-event-nav]');
|
|
213
|
+
const dots = doc.querySelector('[data-event-dots]');
|
|
214
|
+
const template = doc.querySelector('#event-template');
|
|
215
|
+
let events = [];
|
|
216
|
+
let slides = [];
|
|
217
|
+
let activeIndex = 0;
|
|
218
|
+
let switching = false;
|
|
219
|
+
let transitionTimer = 0;
|
|
220
|
+
|
|
221
|
+
function stateLabel(result) {
|
|
222
|
+
return result.mode === 'elapsed'
|
|
223
|
+
? '已过去'
|
|
224
|
+
: result.mode === 'today'
|
|
225
|
+
? '就是今天'
|
|
226
|
+
: '还有';
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function ariaLabel(event) {
|
|
230
|
+
return `${event.title},${stateLabel(event.result)} ${event.result.days} 天`;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function readEvents() {
|
|
234
|
+
const source = doc.querySelector('[data-aidget-region="events"]');
|
|
235
|
+
if (!source) throw new Error('缺少事件区域。');
|
|
236
|
+
const items = Array.from(source.querySelectorAll('[data-event]'));
|
|
237
|
+
if (!items.length) throw new Error('至少需要一个倒计时事件。');
|
|
238
|
+
return items.map((item, index) => {
|
|
239
|
+
const title = item.querySelector('[data-event-title]')?.textContent.trim() ?? '';
|
|
240
|
+
const date = item.querySelector('[data-event-date]');
|
|
241
|
+
const note = item.querySelector('[data-event-note]')?.textContent.trim() ?? '';
|
|
242
|
+
const repeat = item.dataset.eventRepeat;
|
|
243
|
+
const rawDate = date?.getAttribute('datetime')?.trim() ?? '';
|
|
244
|
+
if (!title) throw new Error(`第 ${index + 1} 个事件缺少名称。`);
|
|
245
|
+
if (!date || !rawDate) throw new Error(`第 ${index + 1} 个事件缺少日期。`);
|
|
246
|
+
if (!REPEATS.has(repeat)) throw new Error(`第 ${index + 1} 个事件的重复周期无效。`);
|
|
247
|
+
try {
|
|
248
|
+
return { title, rawDate, note, result: calculateCountdown(rawDate, repeat) };
|
|
249
|
+
} catch (error) {
|
|
250
|
+
throw new Error(`第 ${index + 1} 个事件:${error.message}`);
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function buildSlide(event) {
|
|
256
|
+
const slide = template.content.firstElementChild.cloneNode(true);
|
|
257
|
+
const title = slide.querySelector('[data-slide-title]');
|
|
258
|
+
const date = slide.querySelector('[data-slide-date]');
|
|
259
|
+
const note = slide.querySelector('[data-slide-note]');
|
|
260
|
+
const label = slide.querySelector('[data-slide-label]');
|
|
261
|
+
const count = slide.querySelector('[data-slide-count]');
|
|
262
|
+
if (!title || !date || !note || !label || !count) {
|
|
263
|
+
throw new Error('事件模板缺少必要字段。');
|
|
264
|
+
}
|
|
265
|
+
const formattedCount = new Intl.NumberFormat('zh-CN').format(event.result.days);
|
|
266
|
+
title.textContent = event.title;
|
|
267
|
+
date.dateTime = event.rawDate;
|
|
268
|
+
date.textContent = formatDate(event.rawDate);
|
|
269
|
+
note.textContent = event.note;
|
|
270
|
+
note.hidden = !event.note;
|
|
271
|
+
label.textContent = stateLabel(event.result);
|
|
272
|
+
count.textContent = formattedCount;
|
|
273
|
+
count.style.fontSize = `${Math.max(38, 78 - Math.max(0, formattedCount.length - 2) * 7)}px`;
|
|
274
|
+
slide.dataset.mode = event.result.mode;
|
|
275
|
+
slide.setAttribute('aria-hidden', 'true');
|
|
276
|
+
return slide;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function updateDots() {
|
|
280
|
+
dots.querySelectorAll('.event-dot').forEach((dot, index) => {
|
|
281
|
+
dot.classList.toggle('is-current', index === activeIndex);
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function showError(message) {
|
|
286
|
+
if (transitionTimer) window.clearTimeout(transitionTimer);
|
|
287
|
+
transitionTimer = 0;
|
|
288
|
+
switching = false;
|
|
289
|
+
card.dataset.state = 'error';
|
|
290
|
+
card.setAttribute('aria-label', '日期设置有误');
|
|
291
|
+
nav.removeAttribute('aria-busy');
|
|
292
|
+
nav.hidden = true;
|
|
293
|
+
dots.replaceChildren();
|
|
294
|
+
stage.replaceChildren();
|
|
295
|
+
if (!template) {
|
|
296
|
+
stage.textContent = message;
|
|
297
|
+
} else {
|
|
298
|
+
const slide = template.content.firstElementChild.cloneNode(true);
|
|
299
|
+
const title = slide.querySelector('[data-slide-title]');
|
|
300
|
+
const date = slide.querySelector('[data-slide-date]');
|
|
301
|
+
const note = slide.querySelector('[data-slide-note]');
|
|
302
|
+
const label = slide.querySelector('[data-slide-label]');
|
|
303
|
+
const count = slide.querySelector('[data-slide-count]');
|
|
304
|
+
if (!title || !date || !note || !label || !count) {
|
|
305
|
+
throw new Error('事件模板缺少必要字段。');
|
|
306
|
+
}
|
|
307
|
+
title.textContent = '日期设置有误';
|
|
308
|
+
date.hidden = true;
|
|
309
|
+
note.textContent = message;
|
|
310
|
+
note.hidden = false;
|
|
311
|
+
label.textContent = '请检查';
|
|
312
|
+
count.textContent = '!';
|
|
313
|
+
slide.classList.add('is-active');
|
|
314
|
+
slide.setAttribute('aria-hidden', 'false');
|
|
315
|
+
stage.append(slide);
|
|
316
|
+
}
|
|
317
|
+
console.error('倒数日配置错误:', message);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function render(keepIndex = activeIndex) {
|
|
321
|
+
if (transitionTimer) window.clearTimeout(transitionTimer);
|
|
322
|
+
transitionTimer = 0;
|
|
323
|
+
try {
|
|
324
|
+
if (!template) throw new Error('缺少事件模板。');
|
|
325
|
+
events = readEvents();
|
|
326
|
+
slides = events.map(buildSlide);
|
|
327
|
+
stage.replaceChildren(...slides);
|
|
328
|
+
activeIndex = Math.max(0, Math.min(keepIndex, slides.length - 1));
|
|
329
|
+
slides.forEach((slide, index) => {
|
|
330
|
+
slide.classList.toggle('is-active', index === activeIndex);
|
|
331
|
+
slide.setAttribute('aria-hidden', String(index !== activeIndex));
|
|
332
|
+
});
|
|
333
|
+
dots.replaceChildren(
|
|
334
|
+
...events.map(() => {
|
|
335
|
+
const dot = doc.createElement('span');
|
|
336
|
+
dot.className = 'event-dot';
|
|
337
|
+
return dot;
|
|
338
|
+
}),
|
|
339
|
+
);
|
|
340
|
+
updateDots();
|
|
341
|
+
nav.removeAttribute('aria-busy');
|
|
342
|
+
nav.hidden = events.length < 2;
|
|
343
|
+
const active = events[activeIndex];
|
|
344
|
+
card.dataset.state = active.result.mode;
|
|
345
|
+
card.setAttribute('aria-label', ariaLabel(active));
|
|
346
|
+
switching = false;
|
|
347
|
+
} catch (error) {
|
|
348
|
+
showError(error.message);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function switchEvent(index, direction) {
|
|
353
|
+
if (switching || !events.length || index === activeIndex) return;
|
|
354
|
+
const outgoing = slides[activeIndex];
|
|
355
|
+
const incoming = slides[index];
|
|
356
|
+
if (!outgoing || !incoming) return;
|
|
357
|
+
switching = true;
|
|
358
|
+
activeIndex = index;
|
|
359
|
+
const active = events[index];
|
|
360
|
+
card.dataset.state = active.result.mode;
|
|
361
|
+
card.setAttribute('aria-label', ariaLabel(active));
|
|
362
|
+
updateDots();
|
|
363
|
+
nav.setAttribute('aria-busy', 'true');
|
|
364
|
+
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
|
365
|
+
outgoing.classList.remove('is-active');
|
|
366
|
+
outgoing.setAttribute('aria-hidden', 'true');
|
|
367
|
+
incoming.classList.add('is-active');
|
|
368
|
+
incoming.setAttribute('aria-hidden', 'false');
|
|
369
|
+
nav.removeAttribute('aria-busy');
|
|
370
|
+
switching = false;
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
incoming.classList.add('is-active');
|
|
374
|
+
incoming.setAttribute('aria-hidden', 'false');
|
|
375
|
+
incoming.style.zIndex = '3';
|
|
376
|
+
outgoing.style.zIndex = '2';
|
|
377
|
+
incoming.style.pointerEvents = 'none';
|
|
378
|
+
outgoing.style.pointerEvents = 'none';
|
|
379
|
+
const inMotion = incoming.animate(
|
|
380
|
+
[
|
|
381
|
+
{
|
|
382
|
+
opacity: 0,
|
|
383
|
+
transform: `translateX(${direction * 20}px) scale(0.985)`,
|
|
384
|
+
filter: 'blur(7px)',
|
|
385
|
+
},
|
|
386
|
+
{ opacity: 1, transform: 'translateX(0) scale(1)', filter: 'blur(0)' },
|
|
387
|
+
],
|
|
388
|
+
{
|
|
389
|
+
duration: 360,
|
|
390
|
+
delay: 180,
|
|
391
|
+
easing: 'cubic-bezier(0.16, 1, 0.3, 1)',
|
|
392
|
+
fill: 'both',
|
|
393
|
+
},
|
|
394
|
+
);
|
|
395
|
+
const outMotion = outgoing.animate(
|
|
396
|
+
[
|
|
397
|
+
{ opacity: 1, transform: 'translateX(0) scale(1)', filter: 'blur(0)' },
|
|
398
|
+
{
|
|
399
|
+
opacity: 0,
|
|
400
|
+
transform: `translateX(${-direction * 14}px) scale(0.99)`,
|
|
401
|
+
filter: 'blur(4px)',
|
|
402
|
+
},
|
|
403
|
+
],
|
|
404
|
+
{ duration: 200, easing: 'cubic-bezier(0.4, 0, 0.7, 0.2)', fill: 'both' },
|
|
405
|
+
);
|
|
406
|
+
transitionTimer = window.setTimeout(() => {
|
|
407
|
+
transitionTimer = 0;
|
|
408
|
+
inMotion.cancel();
|
|
409
|
+
outMotion.cancel();
|
|
410
|
+
outgoing.classList.remove('is-active');
|
|
411
|
+
outgoing.setAttribute('aria-hidden', 'true');
|
|
412
|
+
incoming.style.zIndex = '';
|
|
413
|
+
outgoing.style.zIndex = '';
|
|
414
|
+
incoming.style.pointerEvents = '';
|
|
415
|
+
outgoing.style.pointerEvents = '';
|
|
416
|
+
nav.removeAttribute('aria-busy');
|
|
417
|
+
switching = false;
|
|
418
|
+
}, 540);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function switchBy(step) {
|
|
422
|
+
if (switching || events.length < 2) return;
|
|
423
|
+
switchEvent((activeIndex + step + events.length) % events.length, step);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function scheduleNextDay() {
|
|
427
|
+
const next = new Date();
|
|
428
|
+
next.setHours(24, 0, 0, 0);
|
|
429
|
+
window.setTimeout(
|
|
430
|
+
() => {
|
|
431
|
+
render();
|
|
432
|
+
scheduleNextDay();
|
|
433
|
+
},
|
|
434
|
+
Math.max(1_000, next.getTime() - Date.now()),
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (window.document) {
|
|
439
|
+
doc.querySelector('[data-nav="previous"]').addEventListener('click', () => switchBy(-1));
|
|
440
|
+
doc.querySelector('[data-nav="next"]').addEventListener('click', () => switchBy(1));
|
|
441
|
+
render();
|
|
442
|
+
window.aidget.ready();
|
|
443
|
+
scheduleNextDay();
|
|
444
|
+
}
|
|
445
|
+
})();
|
|
446
|
+
</script>
|
|
447
|
+
</body>
|
|
448
|
+
</html>
|
package/package.json
CHANGED
|
@@ -1,6 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aidget-community/countdown",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "倒数日卡片,支持多个日子切换。",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": {
|
|
7
|
+
"name": "aidget community"
|
|
8
|
+
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/aidget-ai/widgets.git",
|
|
12
|
+
"directory": "packages/countdown"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"config.json",
|
|
16
|
+
"index.html",
|
|
17
|
+
"style.css",
|
|
18
|
+
"agents.md",
|
|
19
|
+
"assets"
|
|
20
|
+
],
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
}
|
|
24
|
+
}
|
package/style.css
ADDED
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
color-scheme: light dark;
|
|
3
|
+
--paper: #f7f1e6;
|
|
4
|
+
--paper-line: rgb(40 43 60 / 0.11);
|
|
5
|
+
--ink: #24263a;
|
|
6
|
+
--ink-soft: #737282;
|
|
7
|
+
--blue: #354bd1;
|
|
8
|
+
--blue-deep: #293db5;
|
|
9
|
+
--orange: #e97955;
|
|
10
|
+
--white: #fffdf7;
|
|
11
|
+
--shadow: rgb(34 34 53 / 0.12);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
@media (prefers-color-scheme: dark) {
|
|
15
|
+
:root {
|
|
16
|
+
--paper: #23242c;
|
|
17
|
+
--paper-line: rgb(255 255 255 / 0.1);
|
|
18
|
+
--ink: #f6f1e8;
|
|
19
|
+
--ink-soft: #b8b5c0;
|
|
20
|
+
--blue: #5267ed;
|
|
21
|
+
--blue-deep: #4356d5;
|
|
22
|
+
--orange: #fa8c64;
|
|
23
|
+
--white: #fffdf7;
|
|
24
|
+
--shadow: rgb(0 0 0 / 0.3);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
* {
|
|
29
|
+
box-sizing: border-box;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
html,
|
|
33
|
+
body {
|
|
34
|
+
width: 100%;
|
|
35
|
+
height: 100%;
|
|
36
|
+
margin: 0;
|
|
37
|
+
overflow: hidden;
|
|
38
|
+
background: transparent;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
body {
|
|
42
|
+
color: var(--ink);
|
|
43
|
+
font-family: 'Avenir Next', 'Segoe UI Variable', 'PingFang SC', sans-serif;
|
|
44
|
+
-webkit-font-smoothing: antialiased;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.card {
|
|
48
|
+
position: relative;
|
|
49
|
+
isolation: isolate;
|
|
50
|
+
width: 100%;
|
|
51
|
+
height: 100%;
|
|
52
|
+
padding: 20px 23px;
|
|
53
|
+
overflow: hidden;
|
|
54
|
+
border: 1px solid var(--paper-line);
|
|
55
|
+
border-radius: 21px;
|
|
56
|
+
background: var(--paper);
|
|
57
|
+
box-shadow:
|
|
58
|
+
inset 0 1px 0 rgb(255 255 255 / 0.72),
|
|
59
|
+
0 9px 26px var(--shadow);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.blue-swatch {
|
|
63
|
+
position: absolute;
|
|
64
|
+
z-index: -1;
|
|
65
|
+
top: 14px;
|
|
66
|
+
right: 14px;
|
|
67
|
+
bottom: 14px;
|
|
68
|
+
width: 45%;
|
|
69
|
+
overflow: hidden;
|
|
70
|
+
border-radius: 4px 16px 16px 4px;
|
|
71
|
+
background:
|
|
72
|
+
linear-gradient(145deg, rgb(255 255 255 / 0.09), transparent 42%),
|
|
73
|
+
linear-gradient(155deg, var(--blue), var(--blue-deep));
|
|
74
|
+
box-shadow:
|
|
75
|
+
inset 0 1px 0 rgb(255 255 255 / 0.24),
|
|
76
|
+
0 4px 12px rgb(42 57 156 / 0.16);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.blue-swatch::after {
|
|
80
|
+
position: absolute;
|
|
81
|
+
right: -22px;
|
|
82
|
+
bottom: -30px;
|
|
83
|
+
width: 102px;
|
|
84
|
+
height: 102px;
|
|
85
|
+
border: 1px solid rgb(255 255 255 / 0.17);
|
|
86
|
+
border-radius: 50%;
|
|
87
|
+
content: '';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.orange-swatch {
|
|
91
|
+
position: absolute;
|
|
92
|
+
z-index: 2;
|
|
93
|
+
top: 24px;
|
|
94
|
+
right: calc(45% - 2px);
|
|
95
|
+
width: 17px;
|
|
96
|
+
height: 17px;
|
|
97
|
+
border: 2px solid var(--paper);
|
|
98
|
+
border-radius: 2px;
|
|
99
|
+
background: var(--orange);
|
|
100
|
+
box-shadow: 0 2px 6px rgb(33 34 50 / 0.16);
|
|
101
|
+
transform: rotate(12deg);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.stage {
|
|
105
|
+
position: absolute;
|
|
106
|
+
z-index: 1;
|
|
107
|
+
inset: 0;
|
|
108
|
+
overflow: hidden;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.event {
|
|
112
|
+
position: absolute;
|
|
113
|
+
inset: 0;
|
|
114
|
+
opacity: 0;
|
|
115
|
+
visibility: hidden;
|
|
116
|
+
pointer-events: none;
|
|
117
|
+
will-change: opacity, transform, filter;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.event.is-active {
|
|
121
|
+
opacity: 1;
|
|
122
|
+
visibility: visible;
|
|
123
|
+
pointer-events: auto;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.event-copy {
|
|
127
|
+
position: absolute;
|
|
128
|
+
z-index: 1;
|
|
129
|
+
top: 32px;
|
|
130
|
+
bottom: 52px;
|
|
131
|
+
left: 23px;
|
|
132
|
+
display: flex;
|
|
133
|
+
width: 44%;
|
|
134
|
+
flex-direction: column;
|
|
135
|
+
align-items: flex-start;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.event-title {
|
|
139
|
+
display: -webkit-box;
|
|
140
|
+
max-width: 100%;
|
|
141
|
+
margin: 0;
|
|
142
|
+
overflow: hidden;
|
|
143
|
+
color: var(--ink);
|
|
144
|
+
font-family: 'Songti SC', 'Noto Serif SC', 'Iowan Old Style', serif;
|
|
145
|
+
font-size: clamp(22px, 7.8vw, 28px);
|
|
146
|
+
font-weight: 600;
|
|
147
|
+
line-height: 1.25;
|
|
148
|
+
letter-spacing: 0.015em;
|
|
149
|
+
overflow-wrap: anywhere;
|
|
150
|
+
-webkit-box-orient: vertical;
|
|
151
|
+
-webkit-line-clamp: 2;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.event-date[hidden],
|
|
155
|
+
.event-note[hidden] {
|
|
156
|
+
display: none;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.event-date {
|
|
160
|
+
margin-top: 10px;
|
|
161
|
+
color: var(--ink-soft);
|
|
162
|
+
font-family: 'SF Mono', 'Menlo', monospace;
|
|
163
|
+
font-size: 9px;
|
|
164
|
+
line-height: 1.4;
|
|
165
|
+
letter-spacing: 0.01em;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.event-note {
|
|
169
|
+
display: -webkit-box;
|
|
170
|
+
max-width: 100%;
|
|
171
|
+
margin: auto 0 0;
|
|
172
|
+
overflow: hidden;
|
|
173
|
+
color: var(--ink-soft);
|
|
174
|
+
font-size: 10px;
|
|
175
|
+
line-height: 1.55;
|
|
176
|
+
text-overflow: ellipsis;
|
|
177
|
+
overflow-wrap: anywhere;
|
|
178
|
+
-webkit-box-orient: vertical;
|
|
179
|
+
-webkit-line-clamp: 2;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.count-panel {
|
|
183
|
+
position: absolute;
|
|
184
|
+
z-index: 1;
|
|
185
|
+
top: 38px;
|
|
186
|
+
right: 20px;
|
|
187
|
+
bottom: 31px;
|
|
188
|
+
display: flex;
|
|
189
|
+
width: 40%;
|
|
190
|
+
flex-direction: column;
|
|
191
|
+
align-items: center;
|
|
192
|
+
justify-content: center;
|
|
193
|
+
color: var(--white);
|
|
194
|
+
text-align: center;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.count-context {
|
|
198
|
+
margin: 0 0 -4px;
|
|
199
|
+
color: rgb(255 253 247 / 0.77);
|
|
200
|
+
font-size: 10px;
|
|
201
|
+
font-weight: 600;
|
|
202
|
+
letter-spacing: 0.12em;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.number-lockup {
|
|
206
|
+
display: flex;
|
|
207
|
+
align-items: baseline;
|
|
208
|
+
justify-content: center;
|
|
209
|
+
max-width: 100%;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.count-number {
|
|
213
|
+
max-width: 100%;
|
|
214
|
+
overflow: hidden;
|
|
215
|
+
font-family: 'Avenir Next Condensed', 'Arial Narrow', 'Helvetica Neue', sans-serif;
|
|
216
|
+
font-size: clamp(38px, 20vw, 78px);
|
|
217
|
+
font-weight: 600;
|
|
218
|
+
line-height: 1;
|
|
219
|
+
letter-spacing: -0.06em;
|
|
220
|
+
text-overflow: clip;
|
|
221
|
+
text-shadow: 0 2px 10px rgb(21 32 104 / 0.16);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
.count-unit {
|
|
225
|
+
margin-left: 4px;
|
|
226
|
+
font-size: 15px;
|
|
227
|
+
font-weight: 600;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.event-nav {
|
|
231
|
+
position: absolute;
|
|
232
|
+
z-index: 3;
|
|
233
|
+
bottom: 14px;
|
|
234
|
+
left: 23px;
|
|
235
|
+
display: flex;
|
|
236
|
+
align-items: center;
|
|
237
|
+
gap: 8px;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
.event-nav[hidden] {
|
|
241
|
+
display: none;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.nav-button {
|
|
245
|
+
display: grid;
|
|
246
|
+
width: 26px;
|
|
247
|
+
height: 26px;
|
|
248
|
+
padding: 0;
|
|
249
|
+
place-items: center;
|
|
250
|
+
border: 1px solid var(--paper-line);
|
|
251
|
+
border-radius: 50%;
|
|
252
|
+
background: transparent;
|
|
253
|
+
color: var(--ink-soft);
|
|
254
|
+
cursor: pointer;
|
|
255
|
+
transition:
|
|
256
|
+
color 180ms ease,
|
|
257
|
+
border-color 180ms ease,
|
|
258
|
+
background-color 180ms ease,
|
|
259
|
+
transform 180ms ease;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.nav-button:hover {
|
|
263
|
+
border-color: rgb(233 121 85 / 0.55);
|
|
264
|
+
background: rgb(255 255 255 / 0.16);
|
|
265
|
+
color: var(--orange);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
.nav-button:active {
|
|
269
|
+
transform: scale(0.94);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
.nav-button:focus-visible {
|
|
273
|
+
outline: 2px solid var(--orange);
|
|
274
|
+
outline-offset: 2px;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.nav-button svg {
|
|
278
|
+
width: 12px;
|
|
279
|
+
height: 12px;
|
|
280
|
+
fill: none;
|
|
281
|
+
stroke: currentColor;
|
|
282
|
+
stroke-linecap: round;
|
|
283
|
+
stroke-linejoin: miter;
|
|
284
|
+
stroke-width: 1.6;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
.event-dots {
|
|
288
|
+
display: flex;
|
|
289
|
+
min-width: 28px;
|
|
290
|
+
align-items: center;
|
|
291
|
+
justify-content: center;
|
|
292
|
+
gap: 4px;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
.event-dot {
|
|
296
|
+
width: 4px;
|
|
297
|
+
height: 4px;
|
|
298
|
+
border-radius: 999px;
|
|
299
|
+
background: var(--ink-soft);
|
|
300
|
+
opacity: 0.35;
|
|
301
|
+
transition:
|
|
302
|
+
width 320ms cubic-bezier(0.16, 1, 0.3, 1),
|
|
303
|
+
opacity 220ms ease,
|
|
304
|
+
background-color 220ms ease;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
.event-dot.is-current {
|
|
308
|
+
width: 12px;
|
|
309
|
+
background: var(--orange);
|
|
310
|
+
opacity: 1;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
.card[data-state='error'] .count-context {
|
|
314
|
+
color: #ffe2d8;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
@media (prefers-color-scheme: dark) {
|
|
318
|
+
.card {
|
|
319
|
+
box-shadow:
|
|
320
|
+
inset 0 1px 0 rgb(255 255 255 / 0.06),
|
|
321
|
+
0 9px 26px var(--shadow);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
@media (max-width: 320px) {
|
|
326
|
+
.card {
|
|
327
|
+
padding-right: 17px;
|
|
328
|
+
padding-left: 17px;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
.event-copy {
|
|
332
|
+
left: 17px;
|
|
333
|
+
width: 44%;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
.event-nav {
|
|
337
|
+
left: 17px;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
.count-panel {
|
|
341
|
+
right: 16px;
|
|
342
|
+
width: 41%;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
@media (prefers-reduced-motion: reduce) {
|
|
347
|
+
.nav-button,
|
|
348
|
+
.event-dot {
|
|
349
|
+
transition: none;
|
|
350
|
+
}
|
|
351
|
+
}
|
package/README.md
DELETED