dsh-calendar 0.10.1 → 0.10.3
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/CHANGELOG.md +8 -0
- package/README.en.md +3 -1
- package/README.md +3 -1
- package/docs/CHANGELOG.en.md +8 -0
- package/docs/USAGE.en.md +4 -0
- package/docs/USAGE.md +4 -0
- package/docs/VALIDATION.md +1 -0
- package/lib/client.js +3 -3
- package/lib/ical.js +2 -0
- package/lib/time.d.ts +3 -0
- package/lib/time.js +26 -0
- package/lib/tools.d.ts +1 -1
- package/lib/tools.js +3 -18
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,14 @@
|
|
|
4
4
|
|
|
5
5
|
[历史英文记录](docs/CHANGELOG.en.md)
|
|
6
6
|
|
|
7
|
+
## 0.10.3 (2026-10-05)
|
|
8
|
+
|
|
9
|
+
- 修复窄窗口中日历悬浮按钮遮挡聊天发送按钮的问题;浮层宽度计入内边距,并限制在视口内,避免左侧内容被裁切。
|
|
10
|
+
|
|
11
|
+
## 0.10.2 (2026-10-05)
|
|
12
|
+
|
|
13
|
+
- 严格验证真实 ISO 日期,拒绝不存在的日期、模糊地域格式与缺少时区的日期时间,防止日程悄悄改期。
|
|
14
|
+
|
|
7
15
|
## 0.10.1 (2026-10-03)
|
|
8
16
|
|
|
9
17
|
- 修复语言切换时菜单和悬浮按钮不更新;日期、星期、重复规则、ICS 告警与连接引导支持英文。
|
package/README.en.md
CHANGED
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
[中文](README.md)
|
|
4
4
|
|
|
5
|
+

|
|
6
|
+
|
|
5
7
|
View, create and reschedule CalDAV events from DSH.
|
|
6
8
|
|
|
7
|
-
[](https://www.npmjs.com/package/dsh-calendar) [](https://www.npmjs.com/package/dsh-calendar) [](https://www.npmjs.com/package/dsh-calendar)
|
|
8
10
|
|
|
9
11
|
## What it does
|
|
10
12
|
|
package/README.md
CHANGED
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
[English](README.en.md)
|
|
4
4
|
|
|
5
|
+

|
|
6
|
+
|
|
5
7
|
连接 CalDAV 日历,在 DSH 中查看、创建和调整日程。
|
|
6
8
|
|
|
7
|
-
[](https://www.npmjs.com/package/dsh-calendar) [](https://www.npmjs.com/package/dsh-calendar) [](https://www.npmjs.com/package/dsh-calendar)
|
|
8
10
|
|
|
9
11
|
## 功能
|
|
10
12
|
|
package/docs/CHANGELOG.en.md
CHANGED
|
@@ -4,6 +4,14 @@
|
|
|
4
4
|
|
|
5
5
|
These English notes preserve the earlier translations. The main changelog contains the consolidated version history.
|
|
6
6
|
|
|
7
|
+
## 0.10.3 (2026-10-05)
|
|
8
|
+
|
|
9
|
+
- Move the calendar launcher clear of the chat send button in narrow windows. Include panel padding in its width and keep its bounds inside the viewport.
|
|
10
|
+
|
|
11
|
+
## 0.10.2 (2026-10-05)
|
|
12
|
+
|
|
13
|
+
- Validate real ISO calendar dates, rejecting nonexistent dates, ambiguous locale formats and datetimes without a timezone.
|
|
14
|
+
|
|
7
15
|
## 0.10.0 (2026-10-03)
|
|
8
16
|
|
|
9
17
|
- Import ICS files or pasted content with an explicit preview, selection and batch confirmation. Preserve recurrence rules, excluded dates, moved exceptions and timezone definitions.
|
package/docs/USAGE.en.md
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
[Overview](../README.en.md) · [Changelog](../CHANGELOG.md) · [Validation](VALIDATION.md)
|
|
4
4
|
|
|
5
|
+
## Current improvements
|
|
6
|
+
|
|
7
|
+
Use YYYY-MM-DD for dates and ISO datetimes with Z or a UTC offset. Invalid dates fail before remote creation or updates. Existing events are preserved.
|
|
8
|
+
|
|
5
9
|
## Installation
|
|
6
10
|
|
|
7
11
|
```bash
|
package/docs/USAGE.md
CHANGED
package/docs/VALIDATION.md
CHANGED
package/lib/client.js
CHANGED
|
@@ -1155,16 +1155,16 @@ function AgendaView(props) {
|
|
|
1155
1155
|
|
|
1156
1156
|
// ───────────────────────────── 悬浮面板的样式补充 ─────────────────────────────
|
|
1157
1157
|
const CSS_EXTRA = [
|
|
1158
|
-
".dshc-fab{position:fixed;right:18px;bottom:
|
|
1158
|
+
".dshc-fab{position:fixed;right:18px;bottom:88px;z-index:55;width:38px;height:38px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.12));background:var(--dsw-alias-bg-layer-1,#fff);color:var(--dsw-alias-label-primary,#111);box-shadow:var(--dsw-elevation-prominent,0 6px 18px rgba(0,0,0,.18));cursor:pointer;display:flex;align-items:center;justify-content:center;opacity:.62;transition:opacity .15s,transform .15s}",
|
|
1159
1159
|
".dshc-fab:hover{opacity:1;transform:translateY(-1px)}",
|
|
1160
1160
|
".dshc-fab.on{opacity:1;background:var(--dsw-alias-button-primary-fill,#2563eb);color:var(--dsw-alias-label-primary-foreground,#fff);border-color:transparent}",
|
|
1161
|
-
".dshc-float{position:fixed;right:18px;bottom:
|
|
1161
|
+
".dshc-float{position:fixed;right:18px;bottom:136px;z-index:56;box-sizing:border-box;width:min(760px,calc(100vw - 36px));max-height:min(calc(100vh - 160px),720px);overflow:auto;background:var(--dsw-alias-bg-layer-1,#fff);border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.1));border-radius:16px;box-shadow:var(--dsw-elevation-prominent,0 18px 48px rgba(0,0,0,.28));padding:14px}",
|
|
1162
1162
|
".dshc-float .dshc-shell{border:0;padding:0;background:transparent}",
|
|
1163
1163
|
".dshc-steps{margin:2px 0 10px;font-size:12px;color:var(--dsw-alias-label-tertiary,#6b7280)}",
|
|
1164
1164
|
".dshc-steps summary{cursor:pointer;color:var(--dsw-alias-state-business-primary,#2563eb)}",
|
|
1165
1165
|
".dshc-steps ol{margin:6px 0 0;padding-left:18px;line-height:1.6}",
|
|
1166
1166
|
".dshc-link{color:var(--dsw-alias-state-business-primary,#2563eb);text-decoration:none}",
|
|
1167
|
-
".dshc-toast{position:fixed;right:18px;bottom:
|
|
1167
|
+
".dshc-toast{position:fixed;right:18px;bottom:182px;z-index:57;background:var(--dsw-alias-label-primary,#111);color:var(--dsw-alias-label-primary-foreground,#fff);border-radius:10px;padding:8px 12px;font-size:12px;box-shadow:var(--dsw-elevation-prominent,0 8px 20px rgba(0,0,0,.25))}"
|
|
1168
1168
|
].join("");
|
|
1169
1169
|
|
|
1170
1170
|
// ───────────────────────────── 数据 ─────────────────────────────
|
package/lib/ical.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { randomUUID } from 'node:crypto';
|
|
8
8
|
import ICAL from 'ical.js';
|
|
9
|
+
import { assertIsoTime } from './time.js';
|
|
9
10
|
/** 展开重复事件超出迭代预算:显式报错,避免把「没走到窗口」静默当成「窗口内没有实例」。 */
|
|
10
11
|
export class ExpansionLimitError extends Error {
|
|
11
12
|
constructor(message) {
|
|
@@ -445,6 +446,7 @@ export function updateICalString(data, changes) {
|
|
|
445
446
|
}
|
|
446
447
|
/** 把 ISO 字符串解析成 ICAL.Time(全天 YYYY-MM-DD 或转 UTC 的定时时间)。 */
|
|
447
448
|
function parseTime(value, allDay) {
|
|
449
|
+
assertIsoTime(value, '时间');
|
|
448
450
|
if (allDay || isDateOnly(value)) {
|
|
449
451
|
return ICAL.Time.fromDateString(value);
|
|
450
452
|
}
|
package/lib/time.d.ts
ADDED
package/lib/time.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** Validate calendar dates before Date/ical.js can silently normalize them. */
|
|
2
|
+
export const DATE_ONLY_PATTERN = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
3
|
+
const DATETIME_PATTERN = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{1,9}))?)?(Z|[+-]\d{2}:\d{2})$/;
|
|
4
|
+
export function assertIsoTime(value, label) {
|
|
5
|
+
const dateOnly = DATE_ONLY_PATTERN.exec(value);
|
|
6
|
+
const parts = dateOnly ?? DATETIME_PATTERN.exec(value);
|
|
7
|
+
const invalid = () => new Error(label + ' 不是合法的 ISO 8601 时间:' + value + '。必须是真实存在的日期;定时事件须含时区,例如 2025-01-01T09:00:00+08:00 或 2025-01-01T09:00:00Z。');
|
|
8
|
+
if (!parts)
|
|
9
|
+
throw invalid();
|
|
10
|
+
const year = Number(parts[1]), month = Number(parts[2]), day = Number(parts[3]);
|
|
11
|
+
const leap = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
|
|
12
|
+
const days = [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
13
|
+
if (year < 1 || month < 1 || month > 12 || day < 1 || day > days[month - 1])
|
|
14
|
+
throw invalid();
|
|
15
|
+
if (dateOnly)
|
|
16
|
+
return;
|
|
17
|
+
const hour = Number(parts[4]), minute = Number(parts[5]), second = Number(parts[6] ?? 0);
|
|
18
|
+
// ISO 8601 permits 24:00 only as midnight at the end of this valid date.
|
|
19
|
+
if (hour > 24 || minute > 59 || second > 59 || (hour === 24 && (minute !== 0 || second !== 0 || /[1-9]/.test(parts[7] ?? ''))))
|
|
20
|
+
throw invalid();
|
|
21
|
+
const zone = parts[8];
|
|
22
|
+
if (zone !== 'Z' && (Number(zone.slice(1, 3)) > 23 || Number(zone.slice(4)) > 59))
|
|
23
|
+
throw invalid();
|
|
24
|
+
if (Number.isNaN(new Date(value).getTime()))
|
|
25
|
+
throw invalid();
|
|
26
|
+
}
|
package/lib/tools.d.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { type CalendarConfig } from './config.js';
|
|
8
8
|
import { type CalendarEvent } from './ical.js';
|
|
9
|
+
export { assertIsoTime } from './time.js';
|
|
9
10
|
/** 模型可见的内容块。 */
|
|
10
11
|
export interface ContentBlock {
|
|
11
12
|
type: 'text';
|
|
@@ -29,7 +30,6 @@ export interface CalendarToolDefinition {
|
|
|
29
30
|
}
|
|
30
31
|
export declare function asRecord(args: unknown): Record<string, unknown>;
|
|
31
32
|
export declare function optionalString(args: Record<string, unknown>, key: string): string | undefined;
|
|
32
|
-
export declare function assertIsoTime(value: string, label: string): void;
|
|
33
33
|
export declare function assertTimeRange(start: string, end: string): void;
|
|
34
34
|
export declare function isoNoMillis(value: string): string;
|
|
35
35
|
/** 按开始时间升序稳定排序(CalDAV 服务端返回顺序不保证稳定)。 */
|
package/lib/tools.js
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
import { CalendarService } from './caldav.js';
|
|
8
8
|
import { CALENDAR_PROVIDERS, buildCaldavUrl, resolveAuthMethod, resolveConfig, resolveCredentials, validateOAuthUrl, } from './config.js';
|
|
9
9
|
import { compileParameters } from './parameters.js';
|
|
10
|
+
import { assertIsoTime, DATE_ONLY_PATTERN } from './time.js';
|
|
11
|
+
export { assertIsoTime } from './time.js';
|
|
10
12
|
const EVENT_SCHEMA = { type: 'object', additionalProperties: true };
|
|
11
13
|
const TIMEOUT_MS = 60000;
|
|
12
14
|
export function asRecord(args) {
|
|
@@ -48,28 +50,11 @@ function clampedInteger(args, key, fallback, min, max) {
|
|
|
48
50
|
return fallback;
|
|
49
51
|
return Math.min(max, Math.max(min, value));
|
|
50
52
|
}
|
|
51
|
-
const DATE_ONLY_PATTERN = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
52
|
-
export function assertIsoTime(value, label) {
|
|
53
|
-
const dateOnly = DATE_ONLY_PATTERN.exec(value);
|
|
54
|
-
if (dateOnly !== null) {
|
|
55
|
-
const year = Number(dateOnly[1]);
|
|
56
|
-
const month = Number(dateOnly[2]);
|
|
57
|
-
const day = Number(dateOnly[3]);
|
|
58
|
-
const date = new Date(Date.UTC(year, month - 1, day));
|
|
59
|
-
if (date.getUTCFullYear() !== year || date.getUTCMonth() !== month - 1 || date.getUTCDate() !== day) {
|
|
60
|
-
throw new Error(label + ' 不是合法的日期:' + value + '(YYYY-MM-DD 必须是真实存在的日期)。');
|
|
61
|
-
}
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
if (Number.isNaN(new Date(value).getTime())) {
|
|
65
|
-
throw new Error(label + ' 不是合法的 ISO 8601 时间,例如 2025-01-01T09:00:00+08:00 或 2025-01-01T09:00:00Z。');
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
53
|
/** 日期/时间统一转 epoch 毫秒(date-only 按 UTC 零点),用于范围比较。 */
|
|
69
54
|
function toEpochMs(value) {
|
|
70
55
|
const dateOnly = DATE_ONLY_PATTERN.exec(value);
|
|
71
56
|
if (dateOnly !== null) {
|
|
72
|
-
return Date
|
|
57
|
+
return new Date(value + 'T00:00:00Z').getTime();
|
|
73
58
|
}
|
|
74
59
|
return new Date(value).getTime();
|
|
75
60
|
}
|