dsh-calendar 0.10.2 → 0.10.4
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 +10 -0
- package/README.en.md +5 -0
- package/README.md +5 -0
- package/docs/CHANGELOG.en.md +4 -0
- package/docs/VALIDATION.md +3 -0
- package/docs/validation/0.10.4.md +11 -0
- package/lib/client.js +27 -22
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,16 @@
|
|
|
4
4
|
|
|
5
5
|
[历史英文记录](docs/CHANGELOG.en.md)
|
|
6
6
|
|
|
7
|
+
## 0.10.4 (2026-10-17)
|
|
8
|
+
|
|
9
|
+
- 日程与连接表单采用每个实例独立的字段标识,点击标签可聚焦输入框,并提供正确的可访问名称。
|
|
10
|
+
- 修复英文服务商选项残留中文;首次连接摘要尚在加载时暂缓打开连接编辑器,避免快速点击看到空白配置。
|
|
11
|
+
- 在官方 DSH 0.2.1-alpha.5 的隔离网页版检查本机 CalDAV 新建日程、连接测试、中英文表单及 390px 布局。
|
|
12
|
+
|
|
13
|
+
## 0.10.3 (2026-10-05)
|
|
14
|
+
|
|
15
|
+
- 修复窄窗口中日历悬浮按钮遮挡聊天发送按钮的问题;浮层宽度计入内边距,并限制在视口内,避免左侧内容被裁切。
|
|
16
|
+
|
|
7
17
|
## 0.10.2 (2026-10-05)
|
|
8
18
|
|
|
9
19
|
- 严格验证真实 ISO 日期,拒绝不存在的日期、模糊地域格式与缺少时区的日期时间,防止日程悄悄改期。
|
package/README.en.md
CHANGED
|
@@ -8,6 +8,11 @@ View, create and reschedule CalDAV events from DSH.
|
|
|
8
8
|
|
|
9
9
|
[](https://www.npmjs.com/package/dsh-calendar) [](https://www.npmjs.com/package/dsh-calendar)
|
|
10
10
|
|
|
11
|
+
Feedback and contributions are welcome: report [issues](https://github.com/STARDUSTLC666/dsh-calendar/issues) or submit [pull requests](https://github.com/STARDUSTLC666/dsh-calendar/pulls).
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
See the [0.10.4 validation record](docs/validation/0.10.4.md) for DSH `0.2.1-alpha.5` browser checks and the remaining limits.
|
|
15
|
+
|
|
11
16
|
## What it does
|
|
12
17
|
|
|
13
18
|
- Month, week and agenda views with drag and keyboard rescheduling.
|
package/README.md
CHANGED
|
@@ -8,6 +8,11 @@
|
|
|
8
8
|
|
|
9
9
|
[](https://www.npmjs.com/package/dsh-calendar) [](https://www.npmjs.com/package/dsh-calendar)
|
|
10
10
|
|
|
11
|
+
欢迎使用,遇到问题或有改进建议,请提交 [issues](https://github.com/STARDUSTLC666/dsh-calendar/issues) 和 [PR](https://github.com/STARDUSTLC666/dsh-calendar/pulls)。
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
DSH `0.2.1-alpha.5` 的本轮检查与未验收范围见 [0.10.4 验收记录](docs/validation/0.10.4.md)。
|
|
15
|
+
|
|
11
16
|
## 功能
|
|
12
17
|
|
|
13
18
|
- 月、周与议程视图,支持拖拽和键盘改期。
|
package/docs/CHANGELOG.en.md
CHANGED
|
@@ -4,6 +4,10 @@
|
|
|
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
|
+
|
|
7
11
|
## 0.10.2 (2026-10-05)
|
|
8
12
|
|
|
9
13
|
- Validate real ISO calendar dates, rejecting nonexistent dates, ambiguous locale formats and datetimes without a timezone.
|
package/docs/VALIDATION.md
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
# dsh-calendar 验证记录
|
|
2
2
|
|
|
3
|
+
[0.10.4 · DSH 0.2.1-alpha.5 浏览器操作验收](validation/0.10.4.md)。
|
|
4
|
+
|
|
3
5
|
[0.10.1 中英文界面验收](validation-language-2026-10-03.md)。
|
|
4
6
|
|
|
5
7
|
本页整理原 README 的历史验证说明,保留当时的版本、日期与范围。自动测试、启动检查、浏览器操作和真实服务验收分别记录,不能相互替代。更详细的版本验收文件仍保留在仓库中。
|
|
6
8
|
|
|
7
9
|
## 当前版本
|
|
8
10
|
|
|
11
|
+
- **0.10.3 / 2026-10-05**:Windows 自动测试 194 通过。官方 npm Harness `0.2.1-alpha.1` 网页实际操作完成日程创建(隔离 CalDAV 服务),并在 424×612 窗口复测浮层与发送按钮:浮层在视口内,按钮区域无交集,聊天消息可正常发送。外部 CalDAV 账号和桌面原生窗口未重新验收。
|
|
9
12
|
- [0.10.0:ICS 导入、失败重试与窄屏操作验收](validation/0.10.0.md)
|
|
10
13
|
|
|
11
14
|
## 原中文记录
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# 0.10.4 · DSH alpha.5 网页验收
|
|
2
|
+
|
|
3
|
+
宿主为公开 npm `@deepseek-ai/dsh@0.2.1-alpha.5`,Windows Node 24,系统 Chrome 无头浏览器。测试数据与 DSH profile 均位于 E 盘的隔离目录。
|
|
4
|
+
|
|
5
|
+
- 194 项 Windows 自动测试通过。
|
|
6
|
+
- 日程标题、日期、起止时间、重复、地点及备注可通过标签定位;实际点击日期标签后输入框获得焦点。
|
|
7
|
+
- 新建日程经真实 HTTP CalDAV 客户端写入本机合成服务并重新读取;连接设置可测试该服务。
|
|
8
|
+
- 英文服务商显示 Custom CalDAV,表单字段有英文名称;390px 英文弹窗保持在视口内。
|
|
9
|
+
- 连接摘要请求完成前,连接入口保持禁用,避免编辑器以空配置初始化。
|
|
10
|
+
|
|
11
|
+
未使用外部 CalDAV 账号,未操作桌面原生窗口;本机合成服务通过不等于云端账号验收。
|
package/lib/client.js
CHANGED
|
@@ -10,7 +10,7 @@ var module = { exports: {} }; var exports = module.exports;
|
|
|
10
10
|
"use strict";
|
|
11
11
|
|
|
12
12
|
const React = require("react");
|
|
13
|
-
const { useState, useEffect, useMemo, useRef, useCallback } = React;
|
|
13
|
+
const { useState, useEffect, useMemo, useRef, useCallback, useId } = React;
|
|
14
14
|
const h = React.createElement;
|
|
15
15
|
|
|
16
16
|
// Resolve against the Harness document base, including any reverse-proxy prefix.
|
|
@@ -183,6 +183,7 @@ const UI = {
|
|
|
183
183
|
"conn.title": "CalDAV 连接设置",
|
|
184
184
|
"conn.intro": "测试连接不会修改日历。连接成功后,点击“保存并启用”。",
|
|
185
185
|
"conn.provider": "服务商",
|
|
186
|
+
"provider.custom": "自建 CalDAV",
|
|
186
187
|
"conn.caldavUrl": "日历集合 URL",
|
|
187
188
|
"conn.username": "账号",
|
|
188
189
|
"conn.password": "密码 / 应用专用密码",
|
|
@@ -278,6 +279,7 @@ const UI = {
|
|
|
278
279
|
"conn.title": "CalDAV connection",
|
|
279
280
|
"conn.intro": "Testing the connection does not change your calendar. Once it succeeds, click Save and enable.",
|
|
280
281
|
"conn.provider": "Provider",
|
|
282
|
+
"provider.custom": "Custom CalDAV",
|
|
281
283
|
"conn.caldavUrl": "Calendar collection URL",
|
|
282
284
|
"conn.username": "Account",
|
|
283
285
|
"conn.password": "Password / app-specific password",
|
|
@@ -1155,16 +1157,16 @@ function AgendaView(props) {
|
|
|
1155
1157
|
|
|
1156
1158
|
// ───────────────────────────── 悬浮面板的样式补充 ─────────────────────────────
|
|
1157
1159
|
const CSS_EXTRA = [
|
|
1158
|
-
".dshc-fab{position:fixed;right:18px;bottom:
|
|
1160
|
+
".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
1161
|
".dshc-fab:hover{opacity:1;transform:translateY(-1px)}",
|
|
1160
1162
|
".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:
|
|
1163
|
+
".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
1164
|
".dshc-float .dshc-shell{border:0;padding:0;background:transparent}",
|
|
1163
1165
|
".dshc-steps{margin:2px 0 10px;font-size:12px;color:var(--dsw-alias-label-tertiary,#6b7280)}",
|
|
1164
1166
|
".dshc-steps summary{cursor:pointer;color:var(--dsw-alias-state-business-primary,#2563eb)}",
|
|
1165
1167
|
".dshc-steps ol{margin:6px 0 0;padding-left:18px;line-height:1.6}",
|
|
1166
1168
|
".dshc-link{color:var(--dsw-alias-state-business-primary,#2563eb);text-decoration:none}",
|
|
1167
|
-
".dshc-toast{position:fixed;right:18px;bottom:
|
|
1169
|
+
".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
1170
|
].join("");
|
|
1169
1171
|
|
|
1170
1172
|
// ───────────────────────────── 数据 ─────────────────────────────
|
|
@@ -1310,6 +1312,7 @@ function asPseudoEvent(draft) {
|
|
|
1310
1312
|
|
|
1311
1313
|
function EventForm(props) {
|
|
1312
1314
|
const { draft, events, onCancel, onSubmit, busy, error } = props;
|
|
1315
|
+
const formId = useId();
|
|
1313
1316
|
const [form, setForm] = useState(draft);
|
|
1314
1317
|
useEffect(() => { setForm(draft); }, [draft.uid, draft.date, draft.startTime, draft.summary]);
|
|
1315
1318
|
const patch = (fields) => setForm((prev) => Object.assign({}, prev, fields));
|
|
@@ -1321,21 +1324,21 @@ function EventForm(props) {
|
|
|
1321
1324
|
const body = h("div", null, [
|
|
1322
1325
|
error ? h("div", { className: "dshc-alert error" }, error) : null,
|
|
1323
1326
|
h("div", { className: "dshc-field" }, [
|
|
1324
|
-
h("label",
|
|
1325
|
-
h(Txt, { value: form.summary, autoFocus: true, onChange: (e) => patch({ summary: e.target.value }), placeholder: t("form.newTitle") })
|
|
1327
|
+
h("label", { htmlFor: formId + "-summary" }, t("field.summary")),
|
|
1328
|
+
h(Txt, { id: formId + "-summary", value: form.summary, autoFocus: true, onChange: (e) => patch({ summary: e.target.value }), placeholder: t("form.newTitle") })
|
|
1326
1329
|
]),
|
|
1327
1330
|
h("div", { className: "dshc-field" }, [
|
|
1328
|
-
h("label",
|
|
1329
|
-
h(Txt, { type: "date", value: form.date, onChange: (e) => patch({ date: e.target.value }) })
|
|
1331
|
+
h("label", { htmlFor: formId + "-date" }, t("field.date")),
|
|
1332
|
+
h(Txt, { id: formId + "-date", type: "date", value: form.date, onChange: (e) => patch({ date: e.target.value }) })
|
|
1330
1333
|
]),
|
|
1331
1334
|
form.allDay === true ? null : h("div", { className: "dshc-two" }, [
|
|
1332
|
-
h("div", { className: "dshc-field" }, [h("label",
|
|
1333
|
-
h("div", { className: "dshc-field" }, [h("label",
|
|
1335
|
+
h("div", { className: "dshc-field" }, [h("label", { htmlFor: formId + "-start" }, t("field.start")), h(Txt, { id: formId + "-start", type: "time", value: form.startTime, onChange: (e) => patch({ startTime: e.target.value }) })]),
|
|
1336
|
+
h("div", { className: "dshc-field" }, [h("label", { htmlFor: formId + "-end" }, t("field.end")), h(Txt, { id: formId + "-end", type: "time", value: form.endTime, onChange: (e) => patch({ endTime: e.target.value }) })])
|
|
1334
1337
|
]),
|
|
1335
1338
|
h("div", { className: "dshc-field" }, [h(Chk, { checked: form.allDay === true, label: t("field.allDay"), onChange: (next) => patch({ allDay: next }) })]),
|
|
1336
1339
|
h("div", { className: "dshc-field" }, [
|
|
1337
|
-
h("label",
|
|
1338
|
-
h("select", { className: "dshc-input", value: form.repeat, onChange: (e) => patch({ repeat: e.target.value }) }, [
|
|
1340
|
+
h("label", { htmlFor: formId + "-repeat" }, t("field.repeat")),
|
|
1341
|
+
h("select", { id: formId + "-repeat", className: "dshc-input", value: form.repeat, onChange: (e) => patch({ repeat: e.target.value }) }, [
|
|
1339
1342
|
h("option", { value: "", key: "none" }, t("repeat.none")),
|
|
1340
1343
|
h("option", { value: "daily", key: "d" }, t("repeat.daily")),
|
|
1341
1344
|
h("option", { value: "weekly", key: "w" }, t("repeat.weekly")),
|
|
@@ -1345,13 +1348,13 @@ function EventForm(props) {
|
|
|
1345
1348
|
])
|
|
1346
1349
|
]),
|
|
1347
1350
|
form.repeat === "custom" ? h("div", { className: "dshc-field" }, [
|
|
1348
|
-
h("label",
|
|
1349
|
-
h(Txt, { value: form.rrule, placeholder: "FREQ=WEEKLY;COUNT=10", onChange: (e) => patch({ rrule: e.target.value }) })
|
|
1351
|
+
h("label", { htmlFor: formId + "-rrule" }, "RRULE"),
|
|
1352
|
+
h(Txt, { id: formId + "-rrule", value: form.rrule, placeholder: "FREQ=WEEKLY;COUNT=10", onChange: (e) => patch({ rrule: e.target.value }) })
|
|
1350
1353
|
]) : null,
|
|
1351
|
-
h("div", { className: "dshc-field" }, [h("label",
|
|
1354
|
+
h("div", { className: "dshc-field" }, [h("label", { htmlFor: formId + "-location" }, t("field.location")), h(Txt, { id: formId + "-location", value: form.location, onChange: (e) => patch({ location: e.target.value }) })]),
|
|
1352
1355
|
h("div", { className: "dshc-field" }, [
|
|
1353
|
-
h("label",
|
|
1354
|
-
h("textarea", { className: "dshc-input", value: form.description, onChange: (e) => patch({ description: e.target.value }) })
|
|
1356
|
+
h("label", { htmlFor: formId + "-description" }, t("field.description")),
|
|
1357
|
+
h("textarea", { id: formId + "-description", className: "dshc-input", value: form.description, onChange: (e) => patch({ description: e.target.value }) })
|
|
1355
1358
|
]),
|
|
1356
1359
|
conflicts.length > 0 ? h("div", { className: "dshc-alert warn" }, [
|
|
1357
1360
|
h("div", null, [
|
|
@@ -1479,6 +1482,7 @@ function connectionSummary(connection) {
|
|
|
1479
1482
|
*/
|
|
1480
1483
|
function ConnectionForm(props) {
|
|
1481
1484
|
const { connection, onClose, onSaved } = props;
|
|
1485
|
+
const formId = useId();
|
|
1482
1486
|
const [form, setForm] = useState(() => connectionDraftOf(connection));
|
|
1483
1487
|
const [busy, setBusy] = useState("");
|
|
1484
1488
|
const [result, setResult] = useState(null);
|
|
@@ -1533,10 +1537,10 @@ function ConnectionForm(props) {
|
|
|
1533
1537
|
? h("div", { className: "dshc-alert warn" }, t("conn.noSettings"))
|
|
1534
1538
|
: null,
|
|
1535
1539
|
h("div", { className: "dshc-field" }, [
|
|
1536
|
-
h("label",
|
|
1537
|
-
h("select", { className: "dshc-input", "aria-label": t("conn.provider"), value: form.provider, onChange: (e) => patch({ provider: e.target.value }) },
|
|
1540
|
+
h("label", { htmlFor: formId + "-provider" }, t("conn.provider")),
|
|
1541
|
+
h("select", { id: formId + "-provider", className: "dshc-input", "aria-label": t("conn.provider"), value: form.provider, onChange: (e) => patch({ provider: e.target.value }) },
|
|
1538
1542
|
[h("option", { value: "", key: "__none" }, "— " + t("conn.needProvider") + " —")].concat(
|
|
1539
|
-
PROVIDER_NAMES.map((name) => h("option", { value: name, key: name }, name === "custom" ? "
|
|
1543
|
+
PROVIDER_NAMES.map((name) => h("option", { value: name, key: name }, name === "custom" ? t("provider.custom") : name === "icloud" ? "iCloud" : name === "nextcloud" ? "Nextcloud" : "Google"))
|
|
1540
1544
|
))
|
|
1541
1545
|
]),
|
|
1542
1546
|
PROVIDER_HINT[form.provider] ? h("div", { className: "dshc-sub", style: { marginBottom: "8px" } }, t(PROVIDER_HINT[form.provider])) : null,
|
|
@@ -1554,8 +1558,9 @@ function ConnectionForm(props) {
|
|
|
1554
1558
|
|| (key === "refreshToken" && connection.hasRefreshToken === true));
|
|
1555
1559
|
const hintKey = FIELD_HINT[form.provider] ? FIELD_HINT[form.provider][key] : undefined;
|
|
1556
1560
|
return h("div", { className: "dshc-field", key }, [
|
|
1557
|
-
h("label",
|
|
1561
|
+
h("label", { htmlFor: formId + "-" + key }, t(FIELD_LABEL[key])),
|
|
1558
1562
|
h(Txt, {
|
|
1563
|
+
id: formId + "-" + key,
|
|
1559
1564
|
type: secret ? "password" : "text",
|
|
1560
1565
|
"aria-label": t(FIELD_LABEL[key]),
|
|
1561
1566
|
value: form[key] || "",
|
|
@@ -2016,7 +2021,7 @@ function CalendarPanel(props) {
|
|
|
2016
2021
|
h(Btn, { key: "prev", onClick: () => step(-1), "aria-label": t("action.prev") }, "\u2039"),
|
|
2017
2022
|
h(Btn, { key: "today", onClick: () => setAnchor(new Date()) }, t("action.today")),
|
|
2018
2023
|
h(Btn, { key: "next", onClick: () => step(1), "aria-label": t("action.next") }, "\u203a"),
|
|
2019
|
-
h(Btn, { key: "conn", onClick: () => setConnOpen(true) }, t("conn.button")),
|
|
2024
|
+
h(Btn, { key: "conn", disabled: configured === null, onClick: () => setConnOpen(true) }, t("conn.button")),
|
|
2020
2025
|
h(Btn, { key: "import", disabled: demo === true || importBusy, onClick: () => setImportOpen(value => !value) }, t("import.open")),
|
|
2021
2026
|
demo === true
|
|
2022
2027
|
? h(Btn, { key: "exitdemo", onClick: () => setDemo(false) }, t("action.exitDemo"))
|