@stardustlc/dsh-dream 0.3.4 → 0.4.0

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/lib/client.js ADDED
@@ -0,0 +1,931 @@
1
+ // NOTE(维护者请看这里):这个文件就是插件网页端的**源码**,不经过 tsc ——
2
+ // 仓库里没有 src/client.ts,pnpm run build 也不会重写它。改面板请直接改这里,
3
+ // 改完用 node --check lib/client.js 自检(CI 里也会跑这一步)。
4
+ //
5
+ // 面板只做一件事:把梦境日记(dreams.jsonl)画成人能看懂的样子 —— 概览、心境方块、
6
+ // 教训榜、按时间倒序的梦卡片。数据来自插件自己的同源路由
7
+ // GET /_dsh/dsh-dream/journal(由 src/index.ts 挂载);前端不缓存业务数据,刷新即见。
8
+ // 隐私纪律:面板只读,不写文件、不上传;「隐私模式」只做显示层模糊,DOM 仍是明文。
9
+ window.__ModuleLoader__.load({ id: "dsh-dream", factory: (require) => {
10
+ var module = { exports: {} }; var exports = module.exports;
11
+ "use strict";
12
+
13
+ const React = require("react");
14
+ const { useState, useEffect } = React;
15
+ const h = React.createElement;
16
+
17
+ // 相对路径按 document.baseURI 解析:反代挂在 /tools/dsh/ 这类前缀下也不会打偏。
18
+ const ROUTE = "_dsh/dsh-dream/journal";
19
+ const DEFAULT_LIMIT = 50;
20
+ const MORE_LIMIT = 500;
21
+ const FETCH_TIMEOUT_MS = 15000;
22
+ const SEARCH_DEBOUNCE_MS = 300;
23
+ const MOOD_SLOTS = 4;
24
+ const PRIVACY_KEY = "dsh-dream:privacy";
25
+
26
+ // ───────────────────────── 纯工具(不依赖 DOM,测试可直接调) ─────────────────────────
27
+
28
+ function pad2(value) {
29
+ return value < 10 ? "0" + String(value) : String(value);
30
+ }
31
+
32
+ function timestampOf(iso) {
33
+ const value = Date.parse(iso);
34
+ return Number.isFinite(value) ? value : 0;
35
+ }
36
+
37
+ /** 路由 URL:按 document.baseURI 解析,兼容反代前缀;环境不齐时退回相对路径。 */
38
+ function routeUrl(query) {
39
+ const suffix = query === "" || query === undefined || query === null ? "" : "?" + query;
40
+ const relative = ROUTE + suffix;
41
+ try {
42
+ let base = null;
43
+ if (typeof document !== "undefined" && document !== null && typeof document.baseURI === "string" && document.baseURI !== "") {
44
+ base = document.baseURI;
45
+ } else if (typeof location !== "undefined" && location !== null && typeof location.href === "string" && location.href !== "") {
46
+ base = location.href;
47
+ }
48
+ if (base === null) return relative;
49
+ return new URL(relative, base).toString();
50
+ } catch (error) {
51
+ return relative;
52
+ }
53
+ }
54
+
55
+ function errorMessageOf(body, status) {
56
+ if (body !== null && typeof body === "object") {
57
+ if (body.error !== null && typeof body.error === "object" && typeof body.error.message === "string" && body.error.message !== "") return body.error.message;
58
+ if (typeof body.message === "string" && body.message !== "") return body.message;
59
+ }
60
+ return "请求失败(HTTP " + String(status) + ")";
61
+ }
62
+
63
+ /** GET /_dsh/dsh-dream/journal:契约体 { dreams, stats, query, limit }。 */
64
+ async function fetchJournal(query, limit) {
65
+ const params = ["limit=" + String(limit)];
66
+ if (query !== "") params.unshift("q=" + encodeURIComponent(query));
67
+ const init = { credentials: "same-origin" };
68
+ // 请求必须有上限:挂起的 Promise 会让面板一直转圈,表现成「日记打不开」。
69
+ if (typeof AbortSignal !== "undefined" && typeof AbortSignal.timeout === "function") init.signal = AbortSignal.timeout(FETCH_TIMEOUT_MS);
70
+ const res = await fetch(routeUrl(params.join("&")), init);
71
+ let body = null;
72
+ try { body = await res.json(); } catch (error) { body = null; }
73
+ if (body === null || typeof body !== "object") throw new Error("服务器返回的不是 JSON(HTTP " + String(res.status) + ")");
74
+ if (res.ok !== true || body.ok === false) {
75
+ const failure = new Error(errorMessageOf(body, res.status));
76
+ failure.status = res.status;
77
+ failure.code = body.error !== null && typeof body.error === "object" ? body.error.code : undefined;
78
+ throw failure;
79
+ }
80
+ // 契约体就是 { dreams, stats, query, limit };万一被包进 { ok, value } 信封也认。
81
+ const data = Array.isArray(body.dreams) ? body : (body.value !== null && typeof body.value === "object" ? body.value : body);
82
+ return {
83
+ dreams: data.dreams,
84
+ stats: data.stats,
85
+ query: typeof data.query === "string" ? data.query : query,
86
+ limit: typeof data.limit === "number" ? data.limit : limit,
87
+ };
88
+ }
89
+
90
+ function normalizeDreams(value, limit) {
91
+ if (!Array.isArray(value) || limit <= 0) return [];
92
+ const out = [];
93
+ for (const raw of value) {
94
+ if (raw === null || typeof raw !== "object") continue;
95
+ const reflection = typeof raw.reflection === "string" ? raw.reflection : "";
96
+ if (reflection === "") continue;
97
+ out.push({
98
+ id: typeof raw.id === "string" ? raw.id : "",
99
+ at: typeof raw.at === "string" ? raw.at : "",
100
+ reflection: reflection,
101
+ lessons: Array.isArray(raw.lessons) ? raw.lessons.filter((lesson) => typeof lesson === "string" && lesson.trim() !== "") : [],
102
+ mood: typeof raw.mood === "string" && raw.mood.trim() !== "" ? raw.mood.trim() : "平静",
103
+ });
104
+ }
105
+ // 服务端已倒序;这里再兜一次(坏时间排到最后),保证「新梦在前」不被实现细节破坏。
106
+ out.sort((a, b) => timestampOf(b.at) - timestampOf(a.at));
107
+ return out.slice(0, limit);
108
+ }
109
+
110
+ function normalizeStats(value, dreams) {
111
+ const source = value !== null && typeof value === "object" ? value : {};
112
+ const moodCounts = new Map();
113
+ if (source.moods !== null && typeof source.moods === "object") {
114
+ for (const key of Object.keys(source.moods)) {
115
+ const count = source.moods[key];
116
+ if (typeof count === "number" && count > 0) moodCounts.set(key === "" ? "平静" : key, count);
117
+ }
118
+ }
119
+ if (moodCounts.size === 0) for (const dream of dreams) moodCounts.set(dream.mood, (moodCounts.get(dream.mood) || 0) + 1);
120
+ const moods = Array.from(moodCounts.entries())
121
+ .map(([mood, count]) => ({ mood: mood, count: count }))
122
+ .sort((a, b) => b.count - a.count || String(a.mood).localeCompare(String(b.mood), "zh"));
123
+ const total = typeof source.total === "number" && source.total >= 0 ? source.total : dreams.length;
124
+ const topLessons = [];
125
+ if (Array.isArray(source.topLessons)) {
126
+ for (const raw of source.topLessons) {
127
+ if (raw === null || typeof raw !== "object") continue;
128
+ if (typeof raw.lesson !== "string" || raw.lesson.trim() === "") continue;
129
+ topLessons.push({
130
+ lesson: raw.lesson,
131
+ count: typeof raw.count === "number" && raw.count > 0 ? raw.count : 1,
132
+ lastAt: typeof raw.lastAt === "string" ? raw.lastAt : "",
133
+ });
134
+ }
135
+ }
136
+ if (topLessons.length === 0) {
137
+ // 后端没给统计(或还没接上)时按当前页现算一份,面板不至于空着。
138
+ const counts = new Map();
139
+ for (const dream of dreams) for (const lesson of dream.lessons) {
140
+ const key = lesson.toLowerCase();
141
+ counts.set(key, (counts.get(key) || 0) + 1);
142
+ }
143
+ const sorted = Array.from(counts.entries()).sort((a, b) => b[1] - a[1]).slice(0, 10);
144
+ for (const entry of sorted) {
145
+ let original = "";
146
+ let lastAt = "";
147
+ for (const dream of dreams) {
148
+ for (const lesson of dream.lessons) {
149
+ if (lesson.toLowerCase() !== entry[0]) continue;
150
+ if (original === "") original = lesson;
151
+ if (timestampOf(dream.at) > timestampOf(lastAt)) lastAt = dream.at;
152
+ }
153
+ }
154
+ topLessons.push({ lesson: original || entry[0], count: entry[1], lastAt: lastAt });
155
+ }
156
+ }
157
+ return { total: total, moods: moods, topLessons: topLessons };
158
+ }
159
+
160
+ function spanOf(dreams) {
161
+ let min = null;
162
+ let max = null;
163
+ for (const dream of dreams) {
164
+ const ts = timestampOf(dream.at);
165
+ if (ts === 0) continue;
166
+ if (min === null || ts < min) min = ts;
167
+ if (max === null || ts > max) max = ts;
168
+ }
169
+ if (min === null) return null;
170
+ return { from: min, to: max, days: Math.floor((max - min) / 86400000) };
171
+ }
172
+
173
+ function formatDate(iso) {
174
+ const date = new Date(iso);
175
+ if (!Number.isFinite(date.getTime())) return iso === "" ? "时间未知" : iso;
176
+ return date.getFullYear() + "-" + pad2(date.getMonth() + 1) + "-" + pad2(date.getDate());
177
+ }
178
+
179
+ function formatTime(iso) {
180
+ const date = new Date(iso);
181
+ if (!Number.isFinite(date.getTime())) return "";
182
+ return pad2(date.getHours()) + ":" + pad2(date.getMinutes());
183
+ }
184
+
185
+ function shortDate(ms) {
186
+ const date = new Date(ms);
187
+ return pad2(date.getMonth() + 1) + "-" + pad2(date.getDate());
188
+ }
189
+
190
+ /** 跨度的紧凑写法:同年用 MM-DD → MM-DD,跨年才带年份 —— 窄卡片里也不截断。 */
191
+ function spanTextOf(span) {
192
+ const from = new Date(span.from);
193
+ const to = new Date(span.to);
194
+ if (formatDate(from.toISOString()) === formatDate(to.toISOString())) return formatDate(from.toISOString());
195
+ if (from.getFullYear() === to.getFullYear()) return shortDate(span.from) + " → " + shortDate(span.to);
196
+ return formatDate(from.toISOString()) + " → " + formatDate(to.toISOString());
197
+ }
198
+
199
+ function relativeTime(iso, now) {
200
+ const ts = timestampOf(iso);
201
+ if (ts === 0) return "";
202
+ const diff = now.getTime() - ts;
203
+ const minute = 60000;
204
+ const hour = 3600000;
205
+ const day = 86400000;
206
+ if (diff < minute) return "刚刚";
207
+ if (diff < hour) return Math.floor(diff / minute) + " 分钟前";
208
+ if (diff < day) return Math.floor(diff / hour) + " 小时前";
209
+ if (diff < day * 30) return Math.floor(diff / day) + " 天前";
210
+ return formatDate(iso);
211
+ }
212
+
213
+ /** 「最近一次出现」用的短文案:今天/昨天/N 天前/日期。 */
214
+ function recentText(iso, now) {
215
+ const ts = timestampOf(iso);
216
+ if (ts === 0) return "";
217
+ const date = new Date(ts);
218
+ const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
219
+ const that = new Date(date.getFullYear(), date.getMonth(), date.getDate());
220
+ const days = Math.round((today.getTime() - that.getTime()) / 86400000);
221
+ if (days <= 0) return "今天";
222
+ if (days === 1) return "昨天";
223
+ if (days < 30) return String(days) + " 天前";
224
+ return formatDate(iso);
225
+ }
226
+
227
+ /** 卡片头部的相对时间:与日期重复(≥30 天)时返回空串,别重复贴一个日期。 */
228
+ function dreamAgo(dream, now) {
229
+ const text = recentText(dream.at, now);
230
+ return text === formatDate(dream.at) ? "" : text;
231
+ }
232
+
233
+ /** lastAt 缺失时的降级:在已加载的梦里找这条教训最近一次出现。 */
234
+ function localLastAtFor(dreams, lesson) {
235
+ const needle = String(lesson).toLowerCase();
236
+ let best = "";
237
+ for (const dream of dreams) {
238
+ if (timestampOf(dream.at) <= timestampOf(best)) continue;
239
+ for (const item of dream.lessons) {
240
+ if (item.toLowerCase() === needle) { best = dream.at; break; }
241
+ }
242
+ }
243
+ return best;
244
+ }
245
+
246
+ /** 极简行内标记:只认 **加粗**;其余原样,绝不用 dangerouslySetInnerHTML。 */
247
+ const BOLD_RE = /\*\*([^*\n]+)\*\*/g;
248
+ function inlineNodes(text, keyPrefix) {
249
+ const nodes = [];
250
+ let last = 0;
251
+ let index = 0;
252
+ let match;
253
+ BOLD_RE.lastIndex = 0;
254
+ while ((match = BOLD_RE.exec(text)) !== null) {
255
+ if (match.index > last) nodes.push(text.slice(last, match.index));
256
+ nodes.push(h("strong", { key: keyPrefix + "-b" + String(index) }, match[1]));
257
+ last = match.index + match[0].length;
258
+ index += 1;
259
+ }
260
+ if (last < text.length) nodes.push(text.slice(last));
261
+ return nodes;
262
+ }
263
+
264
+ function moodSlot(mood) {
265
+ const text = String(mood);
266
+ let hash = 0;
267
+ for (let i = 0; i < text.length; i++) hash = (hash * 31 + text.charCodeAt(i)) % 997;
268
+ return hash % MOOD_SLOTS;
269
+ }
270
+
271
+ /** 心境 → 颜色槽;0 用 business 默认色,1/2/3 分别是 success/warn/error。 */
272
+ function toneClass(slot) {
273
+ return slot === 0 ? "" : "dshd-tone-" + String(slot % MOOD_SLOTS);
274
+ }
275
+
276
+ function isCollapsible(text) {
277
+ return String(text).split(/\r?\n/).length > 4 || String(text).length > 220;
278
+ }
279
+
280
+ /** 筛选条件归一:坏形状一律降级,不因为外部数据崩面板。 */
281
+ function cleanupFilters(filters) {
282
+ const source = filters !== null && typeof filters === "object" ? filters : {};
283
+ const moods = Array.isArray(source.moods) ? source.moods.filter(function (mood) { return typeof mood === "string" && mood !== ""; }) : [];
284
+ // 契约字段是 withLessons;onlyWithLessons 只是旧写法别名,两个都认。
285
+ return { moods: moods, withLessons: source.withLessons === true || source.onlyWithLessons === true };
286
+ }
287
+
288
+ /** 生效中的筛选项数(用于按钮上的计数):心境每选一个算 1,只看有教训算 1。 */
289
+ function filterCountOf(filters) {
290
+ const clean = cleanupFilters(filters);
291
+ return clean.moods.length + (clean.withLessons === true ? 1 : 0);
292
+ }
293
+
294
+ /** 筛选只作用于「已加载窗口」,不冒充全量;无筛选时原样返回。 */
295
+ function applyFilters(dreams, filters) {
296
+ const list = Array.isArray(dreams) ? dreams : [];
297
+ const clean = cleanupFilters(filters);
298
+ if (filterCountOf(clean) === 0) return list;
299
+ return list.filter(function (dream) {
300
+ if (clean.moods.length > 0 && clean.moods.indexOf(dream.mood) === -1) return false;
301
+ if (clean.withLessons === true && dream.lessons.length === 0) return false;
302
+ return true;
303
+ });
304
+ }
305
+
306
+ /** ≥8 场才值得摆筛选入口;0 场连搜索框都不渲染,同思路。 */
307
+ function shouldShowFilters(loaded) {
308
+ return typeof loaded === "number" && loaded >= 8;
309
+ }
310
+
311
+ /** 已加载的比总数少、且还没到路由上限 500,才需要「显示更早的梦」。 */
312
+ function needsOlderDreams(loaded, total) {
313
+ return typeof loaded === "number" && typeof total === "number" && loaded < total && loaded < MORE_LIMIT;
314
+ }
315
+
316
+ function countTextOf(query, total, count, filterCount, visibleCount) {
317
+ if (filterCount > 0) return "筛选后 " + String(visibleCount) + " / " + String(count) + " 场";
318
+ if (query !== "") return "「" + query + "」命中 " + String(count) + " 场";
319
+ return "共 " + String(total) + " 场梦";
320
+ }
321
+
322
+ function listHintOf(query, count, filterCount, visibleCount) {
323
+ if (filterCount > 0) return "筛选当前已加载的 " + String(count) + " 场 · 命中 " + String(visibleCount) + " 场";
324
+ if (query !== "") return "「" + query + "」命中 " + String(count) + " 场";
325
+ return "新梦在前";
326
+ }
327
+
328
+ function footerTextOf(query, limit, total, count) {
329
+ if (limit > DEFAULT_LIMIT) {
330
+ const capped = limit >= MORE_LIMIT && count < total ? "(已达路由上限)" : "";
331
+ return "最近 " + String(limit) + " 场 / 共 " + String(total) + " 场" + capped;
332
+ }
333
+ return query === "" ? "新梦在前 · 最多显示 " + String(DEFAULT_LIMIT) + " 场" : "搜索结果 · 最多显示 " + String(DEFAULT_LIMIT) + " 场";
334
+ }
335
+
336
+ /** 面板的视图模型:所有文案/数字集中在这里算,组件只负责画。 */
337
+ function viewModelOf(dreams, stats, now) {
338
+ const list = Array.isArray(dreams) ? dreams : [];
339
+ const total = stats !== null && typeof stats === "object" && typeof stats.total === "number" ? stats.total : list.length;
340
+ const moodRows = stats !== null && typeof stats === "object" && Array.isArray(stats.moods) ? stats.moods : [];
341
+ const rawLessons = stats !== null && typeof stats === "object" && Array.isArray(stats.topLessons) ? stats.topLessons : [];
342
+ const lessonTotal = rawLessons.reduce((sum, row) => sum + (typeof row.count === "number" ? row.count : 0), 0);
343
+ // 排序规则(lead 定案):出现次数降序,同次数按 lastAt 降序,再按文本;并列不加奖牌色。
344
+ const sortedLessons = rawLessons.slice().sort((a, b) => (b.count - a.count)
345
+ || (timestampOf(b.lastAt) - timestampOf(a.lastAt))
346
+ || String(a.lesson).localeCompare(String(b.lesson), "zh"));
347
+ const lessonRows = sortedLessons.map((row, index) => {
348
+ const fromStats = timestampOf(row.lastAt) > 0;
349
+ const knownAt = fromStats ? row.lastAt : localLastAtFor(list, row.lesson);
350
+ return {
351
+ rank: index + 1,
352
+ lesson: row.lesson,
353
+ count: row.count,
354
+ share: Math.max(6, Math.round(row.count / Math.max(lessonTotal, 1) * 100)),
355
+ showBar: row.count >= 2,
356
+ candidate: row.count >= 3,
357
+ lastText: knownAt === "" ? "" : (fromStats ? "最近 " : "本页内最近 ") + recentText(knownAt, now),
358
+ lastTitle: knownAt === "" ? "" : formatDate(knownAt) + (formatTime(knownAt) === "" ? "" : " " + formatTime(knownAt))
359
+ + (fromStats ? "" : "(统计未给 lastAt,按本页已加载的 " + String(list.length) + " 场现算)"),
360
+ };
361
+ });
362
+ const span = spanOf(list);
363
+ const spanText = span === null ? "" : spanTextOf(span);
364
+ const spanFull = span === null ? "" : formatDate(new Date(span.from).toISOString()) + " → " + formatDate(new Date(span.to).toISOString());
365
+ const spanHint = span === null ? "" : (span.days === 0 ? "同一天" : "约 " + String(span.days) + " 天");
366
+ const blocks = list.slice().reverse().map((dream, index) => ({
367
+ key: "block-" + (dream.id !== "" ? dream.id : "no-id") + "-" + String(index),
368
+ tone: toneClass(moodSlot(dream.mood)),
369
+ title: formatDate(dream.at) + (formatTime(dream.at) === "" ? "" : " " + formatTime(dream.at)) + " · " + dream.mood,
370
+ }));
371
+ const headline = headlineOf(list, total, span, now);
372
+ return {
373
+ total: total,
374
+ count: list.length,
375
+ showTiles: list.length >= 2,
376
+ moodRows: moodRows,
377
+ lessonRows: lessonRows,
378
+ lessonTotal: lessonTotal,
379
+ blocks: blocks,
380
+ span: span,
381
+ spanText: spanText,
382
+ spanFull: spanFull,
383
+ spanHint: spanHint,
384
+ latestText: list.length === 0 ? "" : relativeTime(list[0].at, now),
385
+ latestDate: list.length === 0 ? "" : formatDate(list[0].at),
386
+ headline: headline,
387
+ moodCount: moodRows.length,
388
+ };
389
+ }
390
+
391
+ /** 标题行:1 场梦有专用文案(不出现「跨度 0 天」);0 场不渲染。 */
392
+ function headlineOf(dreams, total, span, now) {
393
+ if (dreams.length === 0) return "";
394
+ if (dreams.length === 1 && total <= 1) {
395
+ const dream = dreams[0];
396
+ return "第 1 场梦 · " + formatDate(dream.at) + " · 心境「" + dream.mood + "」";
397
+ }
398
+ const parts = [];
399
+ parts.push(total > dreams.length ? "共 " + String(total) + " 场梦 · 本页最近 " + String(dreams.length) + " 场" : String(dreams.length) + " 场梦");
400
+ if (span !== null) parts.push(span.from === span.to ? formatDate(new Date(span.from).toISOString()) : formatDate(new Date(span.from).toISOString()) + " → " + formatDate(new Date(span.to).toISOString()));
401
+ parts.push("最近:" + relativeTime(dreams[0].at, now));
402
+ return parts.join(" · ");
403
+ }
404
+
405
+ function loadPrivacy() {
406
+ try {
407
+ if (typeof window === "undefined" || window === null || window.localStorage === undefined) return false;
408
+ return window.localStorage.getItem(PRIVACY_KEY) === "1";
409
+ } catch (error) {
410
+ return false;
411
+ }
412
+ }
413
+
414
+ function savePrivacy(value) {
415
+ try {
416
+ if (typeof window === "undefined" || window === null || window.localStorage === undefined) return;
417
+ window.localStorage.setItem(PRIVACY_KEY, value === true ? "1" : "0");
418
+ } catch (error) { /* 隐私模式/配额满写不进去就算了,状态只活在内存里 */ }
419
+ }
420
+
421
+ // ───────────────────────── 小组件 ─────────────────────────
422
+
423
+ function Btn(props) {
424
+ return h("button", {
425
+ type: "button",
426
+ className: "dshd-btn" + (props.variant === "primary" ? " primary" : "") + (props.on === true ? " on" : ""),
427
+ onClick: props.onClick,
428
+ disabled: props.disabled === true,
429
+ title: props.title,
430
+ "aria-pressed": props.pressed === undefined ? undefined : props.pressed,
431
+ }, props.children);
432
+ }
433
+
434
+ function StatTile(props) {
435
+ return h("div", { className: "dshd-tile" }, [
436
+ h("div", { className: "dshd-tile-label", key: "l" }, props.label),
437
+ h("div", { className: "dshd-tile-value", key: "v", title: props.title === undefined ? props.value : props.title }, props.value),
438
+ props.hint === "" || props.hint === undefined ? null : h("div", { className: "dshd-tile-hint", key: "h", title: props.hint }, props.hint),
439
+ ]);
440
+ }
441
+
442
+ function MoodCard(props) {
443
+ const vm = props.vm;
444
+ return h("section", { className: "dshd-card" }, [
445
+ h("h4", { key: "t" }, "心境"),
446
+ h("div", { className: "dshd-mblocks", key: "blocks" }, vm.blocks.map((block) => h("span", {
447
+ key: block.key,
448
+ className: "dshd-mblock " + block.tone,
449
+ title: block.title,
450
+ }))),
451
+ vm.count === 1
452
+ ? h("div", { className: "dshd-hint", key: "one" }, "1 场梦,先不谈分布 —— 再做梦会累积。")
453
+ : h("div", { key: "legend" }, [
454
+ h("div", { className: "dshd-note", key: "cap" }, "左 → 右:从早到晚"),
455
+ h("div", { className: "dshd-legend", key: "rows" }, vm.moodRows.map((row, index) => h("div", { className: "dshd-legend-row", key: "mood-" + String(index) + "-" + row.mood }, [
456
+ h("span", { className: "dshd-mblock " + toneClass(moodSlot(row.mood)), key: "d" }),
457
+ h("span", { className: "dshd-legend-name", key: "n", title: row.mood }, row.mood),
458
+ h("span", { className: "dshd-legend-count", key: "c" }, String(row.count) + " 场"),
459
+ ]))),
460
+ ]),
461
+ ]);
462
+ }
463
+
464
+ function LessonCard(props) {
465
+ const vm = props.vm;
466
+ return h("section", { className: "dshd-card" }, [
467
+ h("h4", { key: "t" }, "教训榜"),
468
+ vm.lessonRows.length === 0
469
+ ? h("div", { className: "dshd-hint", key: "e" }, "还没有教训 —— 记梦时带上 lessons,出现次数会在这里累积。")
470
+ : h("div", { key: "rows" }, vm.lessonRows.map((row, index) => h("div", { className: "dshd-lesson", key: "lesson-" + String(index) + "-" + row.lesson }, [
471
+ h("span", { className: "dshd-rank", key: "r" }, String(row.rank)),
472
+ h("span", { className: "dshd-lesson-body", key: "b" }, [
473
+ h("span", { className: "dshd-lesson-text", key: "t" }, row.lesson),
474
+ row.showBar ? h("span", { className: "dshd-track", key: "bar" }, h("span", { className: "dshd-fill", key: "fill", style: { width: String(row.share) + "%" } })) : null,
475
+ ]),
476
+ h("span", { className: "dshd-lesson-meta", key: "m" }, [
477
+ h("span", { className: "dshd-times", key: "c" }, "×", h("b", { key: "n" }, String(row.count))),
478
+ row.lastText === "" ? null : h("span", { className: "dshd-last", key: "l", title: row.lastTitle }, row.lastText),
479
+ row.candidate ? h("span", { className: "dshd-badge", key: "badge" }, "桥接候选") : null,
480
+ ]),
481
+ ]))),
482
+ h("div", { className: "dshd-note", key: "note" }, "次数 = 日记里出现过的次数,不代表重要度;出现 ≥3 次标为「桥接候选」。是否写入 AGENTS.md 由 dream_bridge 决定。最近时间取自 stats.lastAt;统计缺失时按本页已加载的梦现算。"),
483
+ ]);
484
+ }
485
+
486
+ function DreamCard(props) {
487
+ const dream = props.dream;
488
+ const privacy = props.privacy === true;
489
+ const collapsible = isCollapsible(dream.reflection);
490
+ const [expanded, setExpanded] = useState(false);
491
+ const [peek, setPeek] = useState(false);
492
+ const timeText = formatDate(dream.at) + (formatTime(dream.at) === "" ? "" : " " + formatTime(dream.at));
493
+ const clickCard = privacy ? function () { setPeek(peek !== true); } : undefined;
494
+ return h("article", {
495
+ className: "dshd-dream dshd-mood-" + String(moodSlot(dream.mood)) + (privacy && peek === true ? " dshd-peek" : ""),
496
+ onClick: clickCard,
497
+ title: privacy ? "隐私模式:点击卡片临时查看" : undefined,
498
+ }, [
499
+ h("header", { className: "dshd-dream-head", key: "h" }, [
500
+ h("time", { className: "dshd-time", key: "t", dateTime: dream.at === "" ? undefined : dream.at, title: dream.at === "" ? timeText : timeText + " · " + dream.at }, timeText),
501
+ dreamAgo(dream, props.now) === "" ? null : h("span", { className: "dshd-ago", key: "a" }, dreamAgo(dream, props.now)),
502
+ h("span", { className: "dshd-mood", key: "m", title: "心境:" + dream.mood }, [
503
+ h("i", { className: "dshd-mooddot " + toneClass(moodSlot(dream.mood)), key: "d" }),
504
+ dream.mood,
505
+ ]),
506
+ h("span", { className: "dshd-spacer", key: "s" }),
507
+ collapsible ? h("button", {
508
+ type: "button",
509
+ className: "dshd-linkbtn",
510
+ key: "btn",
511
+ onClick: function (event) { if (event !== undefined && event.stopPropagation !== undefined) event.stopPropagation(); setExpanded(expanded !== true); },
512
+ }, expanded === true ? "收起" : "展开全文") : null,
513
+ ]),
514
+ h("div", { className: "dshd-reflection" + (collapsible && expanded !== true ? " dshd-clamp" : ""), key: "r" }, inlineNodes(dream.reflection, "reflection-" + String(props.index))),
515
+ dream.lessons.length === 0 ? null : h("ol", { className: "dshd-lessons", key: "l" }, dream.lessons.map((lesson, index) => h("li", { key: "li-" + String(index) + "-" + lesson }, [
516
+ h("span", { className: "dshd-num", key: "n" }, String(index + 1)),
517
+ h("span", { className: "dshd-lesson-li", key: "t" }, inlineNodes(lesson, "lesson-" + String(props.index) + "-" + String(index))),
518
+ ]))),
519
+ ]);
520
+ }
521
+
522
+ function EmptyState(props) {
523
+ const moon = h("svg", { width: 34, height: 34, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round", key: "i" }, [
524
+ h("path", { key: "p", d: "M21 12.8A8.5 8.5 0 1 1 11.2 3a6.5 6.5 0 0 0 9.8 9.8z" }),
525
+ ]);
526
+ if (props.query !== undefined && props.query !== "") {
527
+ return h("div", { className: "dshd-empty" }, [
528
+ moon,
529
+ h("h3", { key: "t" }, "没有找到「" + props.query + "」相关的梦"),
530
+ h("p", { key: "p" }, "换个关键词试试,或者清空搜索看全部梦境。"),
531
+ props.onClear ? h("div", { key: "a" }, h(Btn, { onClick: props.onClear }, "清空搜索")) : null,
532
+ ]);
533
+ }
534
+ return h("div", { className: "dshd-empty" }, [
535
+ moon,
536
+ h("h3", { key: "t" }, "还没有梦境日记"),
537
+ h("p", { key: "p" }, "对 agent 说「做个梦」,它会回放最近的会话、反思之后把第一场梦写进这里。"),
538
+ h("div", { className: "dshd-chips", key: "c" }, [
539
+ h("span", { className: "dshd-chip", key: "s1" }, "对 agent 说「做个梦」"),
540
+ h("span", { className: "dshd-chip", key: "s2" }, "dream_digest → dream_save"),
541
+ ]),
542
+ ]);
543
+ }
544
+
545
+ function Skeleton() {
546
+ return h("div", null, [0, 1, 2].map((index) => h("div", { className: "dshd-skel", key: String(index) })));
547
+ }
548
+
549
+ // ───────────────────────── 主面板 ─────────────────────────
550
+
551
+ function DreamPanel() {
552
+ const [dreams, setDreams] = useState([]);
553
+ const [stats, setStats] = useState(null);
554
+ const [loading, setLoading] = useState(true);
555
+ const [error, setError] = useState(null);
556
+ const [queryInput, setQueryInput] = useState("");
557
+ const [query, setQuery] = useState("");
558
+ const [reloadToken, setReloadToken] = useState(0);
559
+ const [now, setNow] = useState(function () { return new Date(); });
560
+ const [privacy, setPrivacy] = useState(loadPrivacy);
561
+ const [filters, setFilters] = useState({ moods: [], withLessons: false });
562
+ const [filterOpen, setFilterOpen] = useState(false);
563
+ const [limit, setLimit] = useState(DEFAULT_LIMIT);
564
+
565
+ // 搜索防抖:连续打字只发一次请求;回车立即生效。
566
+ useEffect(function () {
567
+ const timer = setTimeout(function () { setQuery(queryInput.trim()); }, SEARCH_DEBOUNCE_MS);
568
+ return function () { clearTimeout(timer); };
569
+ }, [queryInput]);
570
+
571
+ useEffect(function () {
572
+ let alive = true;
573
+ setLoading(true);
574
+ setError(null);
575
+ fetchJournal(query, limit).then(function (payload) {
576
+ if (alive !== true) return;
577
+ const list = normalizeDreams(payload.dreams, limit);
578
+ setDreams(list);
579
+ setStats(normalizeStats(payload.stats, list));
580
+ setNow(new Date());
581
+ setLoading(false);
582
+ }).catch(function (failure) {
583
+ if (alive !== true) return;
584
+ setError(failure instanceof Error ? failure : new Error(String(failure)));
585
+ setLoading(false);
586
+ });
587
+ return function () { alive = false; };
588
+ }, [query, reloadToken]);
589
+
590
+ const vm = viewModelOf(dreams, stats, now);
591
+ const pristine = vm.count === 0 && query === "";
592
+ const filterCount = filterCountOf(filters);
593
+ const visibleDreams = applyFilters(dreams, filters);
594
+ const showFilterButton = shouldShowFilters(vm.count) || filterCount > 0;
595
+ const canLoadMore = needsOlderDreams(vm.count, vm.total) && limit < MORE_LIMIT;
596
+
597
+ const reload = function () { setReloadToken(function (value) { return value + 1; }); };
598
+ const clearSearch = function () { setQueryInput(""); setQuery(""); };
599
+ const togglePrivacy = function () {
600
+ const next = privacy !== true;
601
+ setPrivacy(next);
602
+ savePrivacy(next);
603
+ };
604
+ const toggleMoodFilter = function (mood) {
605
+ setFilters(function (prev) {
606
+ const has = prev.moods.indexOf(mood) !== -1;
607
+ return { moods: has === true ? prev.moods.filter(function (item) { return item !== mood; }) : prev.moods.concat([mood]), withLessons: prev.withLessons === true };
608
+ });
609
+ };
610
+ const toggleOnlyLessons = function () {
611
+ setFilters(function (prev) { return { moods: prev.moods, withLessons: prev.withLessons !== true }; });
612
+ };
613
+ const clearFilters = function () { setFilters({ moods: [], withLessons: false }); };
614
+ const showMore = function () { if (limit < MORE_LIMIT) setLimit(MORE_LIMIT); };
615
+
616
+ // 筛选 popover:点面板外任意处收起;纯 node 冒烟没有事件系统就跳过。
617
+ useEffect(function () {
618
+ if (filterOpen !== true) return function () {};
619
+ if (typeof document === "undefined" || document === null || typeof document.addEventListener !== "function") return function () {};
620
+ const onDown = function (event) {
621
+ const pop = document.querySelector(".dshd-filterpop");
622
+ if (pop !== null && typeof pop.contains === "function" && pop.contains(event.target) === true) return;
623
+ setFilterOpen(false);
624
+ };
625
+ document.addEventListener("mousedown", onDown);
626
+ return function () { document.removeEventListener("mousedown", onDown); };
627
+ }, [filterOpen]);
628
+
629
+ const searchIcon = h("svg", { width: 14, height: 14, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 1.8, strokeLinecap: "round", key: "i" }, [
630
+ h("circle", { key: "c", cx: 11, cy: 11, r: 7 }),
631
+ h("path", { key: "l", d: "M20 20l-3.2-3.2" }),
632
+ ]);
633
+
634
+ const toolbar = h("div", { className: "dshd-toolbar", key: "toolbar" }, [
635
+ pristine ? null : h("div", { className: "dshd-search", key: "search" }, [
636
+ searchIcon,
637
+ h("input", {
638
+ key: "search-input",
639
+ className: "dshd-input",
640
+ type: "text",
641
+ value: queryInput,
642
+ placeholder: "搜索反思与教训…",
643
+ "aria-label": "搜索梦境",
644
+ onChange: function (event) { setQueryInput(event.target.value); },
645
+ onKeyDown: function (event) { if (event.key === "Enter") setQuery(event.target.value.trim()); },
646
+ }),
647
+ queryInput === "" ? null : h("button", {
648
+ key: "clear-search",
649
+ type: "button",
650
+ className: "dshd-clear",
651
+ "aria-label": "清空搜索",
652
+ onClick: clearSearch,
653
+ }, "×"),
654
+ ]),
655
+ h("span", { className: "dshd-spacer", key: "spacer" }),
656
+ pristine ? null : h("span", { className: "dshd-hint", key: "count" }, loading === true
657
+ ? "加载中…"
658
+ : countTextOf(query, vm.total, vm.count, filterCount, visibleDreams.length)),
659
+ showFilterButton === true ? h("div", { className: "dshd-filter", key: "filter" }, [
660
+ h(Btn, {
661
+ key: "btn",
662
+ on: filterCount > 0,
663
+ pressed: filterCount > 0,
664
+ onClick: function () { setFilterOpen(filterOpen !== true); },
665
+ title: "筛选作用于已加载窗口,不影响全量统计",
666
+ }, "筛选" + (filterCount > 0 ? " · " + String(filterCount) : "") + " ▾"),
667
+ filterOpen === true ? h("div", { className: "dshd-filterpop", key: "pop", role: "group", "aria-label": "筛选梦境" }, [
668
+ h("div", { className: "dshd-filtertitle", key: "title" }, "筛选当前已加载的 " + String(vm.count) + " 场,不影响全量统计"),
669
+ h("div", { key: "moods" }, vm.moodRows.map(function (row) {
670
+ const on = filters.moods.indexOf(row.mood) !== -1;
671
+ return h("button", {
672
+ key: "mood-" + row.mood,
673
+ type: "button",
674
+ className: "dshd-chipbtn" + (on === true ? " on" : ""),
675
+ "aria-pressed": on === true ? "true" : "false",
676
+ onClick: function () { toggleMoodFilter(row.mood); },
677
+ }, row.mood);
678
+ })),
679
+ h("div", { key: "only" }, h("button", {
680
+ type: "button",
681
+ className: "dshd-chipbtn" + (filters.withLessons === true ? " on" : ""),
682
+ "aria-pressed": filters.withLessons === true ? "true" : "false",
683
+ onClick: toggleOnlyLessons,
684
+ }, "只看有教训的梦")),
685
+ h("div", { className: "dshd-filterfoot", key: "foot" }, [
686
+ h("button", { key: "clearbtn", type: "button", className: "dshd-linkbtn", onClick: clearFilters, disabled: filterCount === 0 }, "清空筛选"),
687
+ h(Btn, { key: "done", onClick: function () { setFilterOpen(false); } }, "完成"),
688
+ ]),
689
+ ]) : null,
690
+ ]) : null,
691
+ pristine ? null : h(Btn, {
692
+ key: "privacy",
693
+ onClick: togglePrivacy,
694
+ on: privacy === true,
695
+ pressed: privacy === true,
696
+ title: "显示层模糊;DOM 仍为明文,不改文件、不加密",
697
+ }, privacy === true ? "隐私模式:开" : "隐私模式"),
698
+ h(Btn, { key: "refresh", onClick: reload, disabled: loading === true, title: "重新读取梦境日记" }, "刷新"),
699
+ ]);
700
+
701
+ const errorBox = error === null ? null : h("div", { className: "dshd-alert error", role: "alert", key: "error" }, [
702
+ h("span", { key: "m" }, "读取梦境日记失败:" + (error.message === "" ? String(error) : error.message)),
703
+ h("span", { className: "dshd-spacer", key: "s" }),
704
+ h(Btn, { key: "retry", onClick: reload, variant: "primary" }, "重试"),
705
+ ]);
706
+
707
+ const body = function () {
708
+ if (loading === true && vm.count === 0) return h(Skeleton, { key: "skeleton" });
709
+ if (vm.count === 0) {
710
+ if (error !== null) return h("div", { className: "dshd-empty", key: "failed" }, [
711
+ h("h3", { key: "t" }, "暂时读不到梦境日记"),
712
+ h("p", { key: "p" }, "等宿主路由就绪后点上面的「重试」;如果一直失败,看一眼 DSH 日志里有没有 [dsh-dream] 告警。"),
713
+ ]);
714
+ return h(EmptyState, { key: "empty", query: query, onClear: query === "" ? null : clearSearch });
715
+ }
716
+ return h("div", { key: "content" }, [
717
+ vm.showTiles
718
+ ? h("div", { className: "dshd-tiles", key: "tiles" }, [
719
+ h(StatTile, { key: "total", label: "梦境总数", value: String(vm.total) + " 场", hint: vm.total > vm.count ? "本页显示最近 " + String(vm.count) + " 场" : "全部梦境日记" }),
720
+ h(StatTile, { key: "span", label: "时间跨度", value: vm.spanText, hint: vm.spanHint, title: vm.spanFull }),
721
+ h(StatTile, { key: "latest", label: "最近一场", value: vm.latestText, hint: vm.latestDate }),
722
+ h(StatTile, { key: "mood", label: "心境", value: String(vm.moodCount) + " 种", hint: vm.moodCount === 0 ? "" : "最常见:" + vm.moodRows[0].mood }),
723
+ ])
724
+ : h("div", { className: "dshd-headline", key: "headline" }, vm.headline),
725
+ h("div", { className: "dshd-two", key: "two" }, [
726
+ h(MoodCard, { key: "mood", vm: vm }),
727
+ h(LessonCard, { key: "lessons", vm: vm }),
728
+ ]),
729
+ h("div", { className: "dshd-listhead", key: "listhead" }, [
730
+ h("h4", { key: "t" }, query === "" && filterCount === 0 ? "梦境时间线" : (query === "" ? "筛选结果" : "搜索结果")),
731
+ h("span", { className: "dshd-hint", key: "n" }, listHintOf(query, vm.count, filterCount, visibleDreams.length)),
732
+ ]),
733
+ visibleDreams.length === 0 && filterCount > 0
734
+ ? h("div", { className: "dshd-filterempty", key: "filtered" }, [
735
+ h("span", { key: "t" }, "没有符合筛选的梦(筛选作用于已加载的 " + String(vm.count) + " 场)。"),
736
+ h("span", { className: "dshd-spacer", key: "s" }),
737
+ h(Btn, { key: "clearbtn", onClick: clearFilters }, "清空筛选"),
738
+ ])
739
+ : h("div", { className: "dshd-timeline", key: "timeline" }, visibleDreams.map(function (dream, index) {
740
+ return h(DreamCard, { key: (dream.id !== "" ? dream.id : "dream") + "#" + String(index), dream: dream, now: now, privacy: privacy, index: index });
741
+ })),
742
+ ]);
743
+ };
744
+
745
+ return h("div", { className: "dshd-root" + (privacy === true ? " dshd-privacy" : ""), key: "root" }, [
746
+ h("div", { className: "dshd-shell", key: "shell" }, [
747
+ errorBox,
748
+ toolbar,
749
+ privacy === true ? h("div", { className: "dshd-note", key: "privacynote", role: "status" }, "隐私模式:显示层模糊;DOM 仍为明文,不改文件、不加密。点击卡片可临时查看。") : null,
750
+ body(),
751
+ h("div", { className: "dshd-footer", key: "footer" }, [
752
+ h("span", { key: "l" }, footerTextOf(query, limit, vm.total, vm.count)),
753
+ h("span", { className: "dshd-spacer", key: "sp" }),
754
+ canLoadMore === true ? h(Btn, { key: "more", onClick: showMore, disabled: loading === true }, loading === true ? "加载中…" : "显示更早的梦") : null,
755
+ h("span", { key: "r" }, "数据来自梦境日记(dreams.jsonl)"),
756
+ ]),
757
+ ]),
758
+ ]);
759
+ }
760
+
761
+ function DreamSection() {
762
+ return h("div", null, [
763
+ h("h2", { key: "t" }, "梦境日记"),
764
+ h("p", { className: "dshd-sub", key: "p" }, "会话回放 → 反思 → 记忆巩固。这里按时间倒序陈列 agent 做过的梦,附心境方块与教训榜。"),
765
+ h(DreamPanel, { key: "panel" }),
766
+ ]);
767
+ }
768
+
769
+ /* @ui-css-start */
770
+ const CSS = [
771
+ ".dshd-root{color:var(--dsw-alias-label-primary,#111);font-size:13px;line-height:1.6}",
772
+ ".dshd-shell{background:var(--dsw-alias-bg-layer-1,#fff);border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.08));border-radius:14px;padding:14px 16px;container-type:inline-size}",
773
+ ".dshd-sub{color:var(--dsw-alias-label-tertiary,#6b7280);font-size:12px;margin:4px 0 12px}",
774
+ "@container (max-width:620px){.dshd-two{grid-template-columns:1fr}.dshd-toolbar .dshd-hint{display:none}}",
775
+ ".dshd-toolbar{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:12px}",
776
+ ".dshd-search{position:relative;display:flex;align-items:center;flex:1;min-width:180px}",
777
+ ".dshd-search svg{position:absolute;left:9px;color:var(--dsw-alias-label-tertiary,#6b7280);pointer-events:none}",
778
+ ".dshd-input{width:100%;box-sizing:border-box;background:var(--dsw-specific-input-major,var(--dsw-alias-bg-layer-2,rgba(0,0,0,.03)));border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.12));border-radius:9px;padding:6px 28px 6px 30px;font:inherit;color:inherit}",
779
+ ".dshd-input:focus{outline:2px solid var(--dsw-alias-state-business-primary,#2563eb);outline-offset:-1px}",
780
+ ".dshd-clear{position:absolute;right:5px;border:0;background:transparent;color:var(--dsw-alias-label-tertiary,#6b7280);font-size:16px;line-height:1;cursor:pointer;padding:2px 6px;border-radius:6px}",
781
+ ".dshd-clear:hover{background:color-mix(in srgb,var(--dsw-alias-label-primary,#111) 8%,transparent);color:var(--dsw-alias-label-primary,#111)}",
782
+ ".dshd-spacer{flex:1}",
783
+ ".dshd-btn{display:inline-flex;align-items:center;gap:4px;border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.1));background:var(--dsw-alias-bg-layer-1,#fff);color:var(--dsw-alias-label-primary,#111);border-radius:9px;padding:5px 11px;font-size:12px;cursor:pointer;transition:background .12s,border-color .12s}",
784
+ ".dshd-btn:hover:not(:disabled){background:color-mix(in srgb,var(--dsw-alias-label-primary,#111) 6%,transparent)}",
785
+ ".dshd-btn:disabled{opacity:.5;cursor:not-allowed}",
786
+ ".dshd-btn.primary{background:var(--dsw-alias-button-primary-fill,#2563eb);border-color:transparent;color:#fff}",
787
+ ".dshd-btn.primary:hover:not(:disabled){background:var(--dsw-alias-button-primary-hover,#1d4ed8)}",
788
+ ".dshd-btn.on{border-color:var(--dsw-alias-state-warn-primary,#f59e0b);background:color-mix(in srgb,var(--dsw-alias-state-warn-primary,#f59e0b) 18%,transparent)}",
789
+ ".dshd-filter{position:relative}",
790
+ ".dshd-filterpop{position:absolute;right:0;top:calc(100% + 6px);z-index:6;width:min(280px,86vw);background:var(--dsw-alias-bg-layer-1,#fff);border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.12));border-radius:12px;padding:10px 11px;box-shadow:var(--dsw-elevation-prominent,0 10px 28px rgba(0,0,0,.18))}",
791
+ ".dshd-filtertitle{font-size:11px;color:var(--dsw-alias-label-tertiary,#6b7280);margin-bottom:7px}",
792
+ ".dshd-chipbtn{display:inline-flex;align-items:center;border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.12));background:transparent;color:var(--dsw-alias-label-primary,#111);border-radius:999px;padding:2px 9px;font-size:12px;cursor:pointer;margin:0 6px 6px 0}",
793
+ ".dshd-chipbtn.on{background:color-mix(in srgb,var(--dsw-alias-state-business-primary,#2563eb) 14%,transparent);border-color:var(--dsw-alias-state-business-primary,#2563eb)}",
794
+ ".dshd-filterfoot{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-top:2px}",
795
+ ".dshd-filterempty{display:flex;align-items:center;gap:8px;border-radius:10px;padding:10px 12px;font-size:12px;margin-bottom:10px;background:var(--dsw-alias-bg-layer-2,rgba(0,0,0,.03));color:var(--dsw-alias-label-tertiary,#6b7280)}",
796
+ ".dshd-hint{color:var(--dsw-alias-label-tertiary,#6b7280);font-size:12px}",
797
+ ".dshd-note{font-size:11px;color:var(--dsw-alias-label-tertiary,#6b7280);margin-top:8px}",
798
+ ".dshd-headline{font-size:13px;font-weight:600;margin:0 0 10px}",
799
+ ".dshd-tiles{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:8px;margin-bottom:12px}",
800
+ ".dshd-tile{background:var(--dsw-alias-bg-layer-2,rgba(0,0,0,.03));border-radius:11px;padding:9px 11px;min-width:0}",
801
+ ".dshd-tile-label{font-size:11px;color:var(--dsw-alias-label-tertiary,#6b7280)}",
802
+ ".dshd-tile-value{font-size:17px;font-weight:600;margin-top:1px;overflow-wrap:anywhere;line-height:1.35}",
803
+ ".dshd-tile-hint{font-size:11px;color:var(--dsw-alias-label-tertiary,#6b7280);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
804
+ ".dshd-two{display:grid;grid-template-columns:minmax(0,3fr) minmax(0,2fr);gap:10px;margin-bottom:16px}",
805
+ ".dshd-card{background:var(--dsw-alias-bg-layer-2,rgba(0,0,0,.03));border-radius:12px;padding:11px 12px;min-width:0}",
806
+ ".dshd-card h4{margin:0 0 8px;font-size:13px;font-weight:600}",
807
+ ".dshd-mblocks{display:flex;flex-wrap:wrap;gap:3px;margin-bottom:8px}",
808
+ ".dshd-mblock{display:inline-block;width:12px;height:12px;border-radius:3px;flex:0 0 auto;background:var(--dsw-alias-state-business-primary,#2563eb)}",
809
+ ".dshd-tone-1{background:var(--dsw-alias-state-success-primary,#16a34a)}",
810
+ ".dshd-tone-2{background:var(--dsw-alias-state-warn-primary,#f59e0b)}",
811
+ ".dshd-tone-3{background:var(--dsw-alias-state-error-primary,#dc2626)}",
812
+ ".dshd-legend{display:flex;flex-direction:column;gap:4px}",
813
+ ".dshd-legend-row{display:flex;align-items:center;gap:7px;font-size:12px}",
814
+ ".dshd-legend-name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
815
+ ".dshd-legend-count{color:var(--dsw-alias-label-tertiary,#6b7280);font-size:11px;font-variant-numeric:tabular-nums}",
816
+ ".dshd-lesson{display:flex;gap:8px;align-items:flex-start;padding:5px 0;border-bottom:1px dashed var(--dsw-alias-border-l2,rgba(0,0,0,.08));flex-wrap:wrap}",
817
+ ".dshd-lesson:last-child{border-bottom:0;padding-bottom:0}",
818
+ ".dshd-rank{flex:0 0 auto;width:18px;height:18px;border-radius:6px;background:var(--dsw-alias-bg-layer-1,#fff);border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.1));color:var(--dsw-alias-label-tertiary,#6b7280);font-size:11px;display:flex;align-items:center;justify-content:center;font-variant-numeric:tabular-nums}",
819
+ ".dshd-lesson-body{flex:1;min-width:140px;display:flex;flex-direction:column;gap:4px}",
820
+ ".dshd-lesson-meta{flex:0 0 auto;display:flex;align-items:center;gap:6px;flex-wrap:wrap}",
821
+ ".dshd-last{font-size:11px;color:var(--dsw-alias-label-tertiary,#6b7280);white-space:nowrap}",
822
+ ".dshd-lesson-text{font-size:12px;word-break:break-word}",
823
+ ".dshd-track{display:block;height:6px;border-radius:999px;background:color-mix(in srgb,var(--dsw-alias-label-primary,#111) 8%,transparent);overflow:hidden}",
824
+ ".dshd-fill{display:block;height:100%;border-radius:999px;background:var(--dsw-alias-state-business-primary,#2563eb);min-width:6px}",
825
+ ".dshd-times{flex:0 0 auto;font-size:11px;color:var(--dsw-alias-label-tertiary,#6b7280);font-variant-numeric:tabular-nums}",
826
+ ".dshd-times b{color:var(--dsw-alias-label-primary,#111)}",
827
+ ".dshd-badge{flex:0 0 auto;font-size:11px;padding:1px 7px;border-radius:999px;background:color-mix(in srgb,var(--dsw-alias-state-warn-primary,#f59e0b) 18%,transparent);color:var(--dsw-alias-label-primary,#111);white-space:nowrap}",
828
+ ".dshd-listhead{display:flex;align-items:baseline;justify-content:space-between;gap:8px;margin:2px 0 8px}",
829
+ ".dshd-listhead h4{margin:0;font-size:13px;font-weight:600}",
830
+ ".dshd-timeline{display:flex;flex-direction:column;gap:8px}",
831
+ ".dshd-dream{background:var(--dsw-alias-bg-layer-2,rgba(0,0,0,.03));border-radius:12px;padding:10px 12px;border-left:3px solid var(--dsw-alias-state-business-primary,#2563eb);min-width:0}",
832
+ ".dshd-dream.dshd-mood-1{border-left-color:var(--dsw-alias-state-success-primary,#16a34a)}",
833
+ ".dshd-dream.dshd-mood-2{border-left-color:var(--dsw-alias-state-warn-primary,#f59e0b)}",
834
+ ".dshd-dream.dshd-mood-3{border-left-color:var(--dsw-alias-state-error-primary,#dc2626)}",
835
+ ".dshd-dream-head{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:5px}",
836
+ ".dshd-time{font-size:12px;color:var(--dsw-alias-label-tertiary,#6b7280);font-variant-numeric:tabular-nums}",
837
+ ".dshd-ago{font-size:11px;color:var(--dsw-alias-label-tertiary,#6b7280)}",
838
+ ".dshd-mood{display:inline-flex;align-items:center;gap:5px;font-size:11px;padding:1px 8px;border-radius:999px;background:color-mix(in srgb,var(--dsw-alias-label-primary,#111) 6%,transparent);color:var(--dsw-alias-label-primary,#111);max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
839
+ ".dshd-mooddot{width:6px;height:6px;border-radius:999px;flex:0 0 auto;background:var(--dsw-alias-state-business-primary,#2563eb)}",
840
+ ".dshd-linkbtn{border:0;background:transparent;color:var(--dsw-alias-state-business-primary,#2563eb);font-size:12px;cursor:pointer;padding:1px 4px;border-radius:6px}",
841
+ ".dshd-linkbtn:hover{background:color-mix(in srgb,var(--dsw-alias-state-business-primary,#2563eb) 10%,transparent)}",
842
+ ".dshd-reflection{white-space:pre-wrap;word-break:break-word}",
843
+ ".dshd-clamp{display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:4;overflow:hidden;max-height:6.4em}",
844
+ ".dshd-lessons{margin:8px 0 0;padding:0;list-style:none;display:flex;flex-direction:column;gap:4px}",
845
+ ".dshd-lessons li{display:flex;gap:7px;align-items:flex-start;font-size:12px}",
846
+ ".dshd-num{flex:0 0 auto;min-width:17px;height:17px;border-radius:5px;background:color-mix(in srgb,var(--dsw-alias-state-business-primary,#2563eb) 14%,transparent);color:var(--dsw-alias-state-business-primary,#2563eb);font-size:11px;display:inline-flex;align-items:center;justify-content:center;font-variant-numeric:tabular-nums}",
847
+ ".dshd-lesson-li{flex:1;min-width:0;word-break:break-word}",
848
+ ".dshd-privacy .dshd-reflection,.dshd-privacy .dshd-lessons{filter:blur(4px)}",
849
+ ".dshd-privacy .dshd-dream.dshd-peek .dshd-reflection,.dshd-privacy .dshd-dream.dshd-peek .dshd-lessons{filter:none}",
850
+ ".dshd-alert{display:flex;gap:8px;align-items:center;border-radius:10px;padding:8px 10px;font-size:12px;margin-bottom:10px;background:color-mix(in srgb,var(--dsw-alias-label-primary,#111) 5%,transparent)}",
851
+ ".dshd-alert.error{background:color-mix(in srgb,var(--dsw-alias-state-error-primary,#dc2626) 12%,transparent);color:var(--dsw-alias-state-error-primary,#dc2626)}",
852
+ ".dshd-empty{padding:28px 18px;text-align:center;color:var(--dsw-alias-label-tertiary,#6b7280)}",
853
+ ".dshd-empty svg{color:var(--dsw-alias-label-tertiary,#6b7280);opacity:.75}",
854
+ ".dshd-empty h3{color:var(--dsw-alias-label-primary,#111);margin:8px 0 6px;font-size:15px}",
855
+ ".dshd-empty p{margin:0 auto 10px;max-width:430px;font-size:12px}",
856
+ ".dshd-chips{display:flex;gap:6px;justify-content:center;flex-wrap:wrap}",
857
+ ".dshd-chip{font-family:ui-monospace,Consolas,monospace;font-size:11px;background:var(--dsw-alias-bg-layer-2,rgba(0,0,0,.05));border-radius:999px;padding:2px 9px;color:var(--dsw-alias-label-tertiary,#6b7280)}",
858
+ ".dshd-skel{height:58px;border-radius:12px;margin-bottom:8px;background:linear-gradient(90deg,var(--dsw-alias-bg-layer-2,rgba(0,0,0,.04)) 25%,color-mix(in srgb,var(--dsw-alias-label-primary,#111) 10%,transparent) 37%,var(--dsw-alias-bg-layer-2,rgba(0,0,0,.04)) 63%);background-size:400% 100%;animation:dshd-shimmer 1.4s ease infinite}",
859
+ "@keyframes dshd-shimmer{0%{background-position:100% 0}100%{background-position:0 0}}",
860
+ ".dshd-footer{margin-top:12px;display:flex;align-items:center;gap:8px;justify-content:space-between;color:var(--dsw-alias-label-tertiary,#6b7280);font-size:11px;flex-wrap:wrap}"
861
+ ].join("");
862
+ /* @ui-css-end */
863
+
864
+ // ───────────────────────── 挂载 ─────────────────────────
865
+
866
+ const inject = ["slots"];
867
+
868
+ function apply(ctx) {
869
+ try {
870
+ ctx.effect(function () {
871
+ const id = "dsh-dream/client";
872
+ if (document.querySelector("style[data-plugin-css='" + id + "']") !== null) return function () {};
873
+ const style = document.createElement("style");
874
+ style.dataset.plugin = "dsh-dream";
875
+ style.dataset.pluginCss = id;
876
+ style.textContent = CSS;
877
+ document.head.appendChild(style);
878
+ return function () { style.remove(); };
879
+ }, "dsh-dream: styles");
880
+ } catch (error) { /* 无 DOM 环境(纯 node 侧冒烟)时跳过 */ }
881
+
882
+ try {
883
+ ctx.slots.inject("settings.section", function () {
884
+ return ctx.slots.register({
885
+ name: "settings.section",
886
+ id: "dsh-dream",
887
+ order: 55,
888
+ label: function () { return "梦境日记"; },
889
+ inject: function () { return {}; },
890
+ }, DreamSection);
891
+ });
892
+ } catch (error) { /* 没有 slots 服务:仍是合法插件,只是没有设置页 */ }
893
+ }
894
+
895
+ exports.apply = apply;
896
+ exports.inject = inject;
897
+ // 纯函数内部件:验证脚本可直接取来测,不必另抄一份算法。
898
+ exports.__internals = {
899
+ routeUrl: routeUrl,
900
+ fetchJournal: fetchJournal,
901
+ normalizeDreams: normalizeDreams,
902
+ normalizeStats: normalizeStats,
903
+ spanOf: spanOf,
904
+ formatDate: formatDate,
905
+ formatTime: formatTime,
906
+ relativeTime: relativeTime,
907
+ inlineNodes: inlineNodes,
908
+ moodSlot: moodSlot,
909
+ toneClass: toneClass,
910
+ isCollapsible: isCollapsible,
911
+ cleanupFilters: cleanupFilters,
912
+ filterCountOf: filterCountOf,
913
+ applyFilters: applyFilters,
914
+ shouldShowFilters: shouldShowFilters,
915
+ needsOlderDreams: needsOlderDreams,
916
+ countTextOf: countTextOf,
917
+ listHintOf: listHintOf,
918
+ footerTextOf: footerTextOf,
919
+ MORE_LIMIT: MORE_LIMIT,
920
+ recentText: recentText,
921
+ dreamAgo: dreamAgo,
922
+ loadPrivacy: loadPrivacy,
923
+ savePrivacy: savePrivacy,
924
+ spanTextOf: spanTextOf,
925
+ localLastAtFor: localLastAtFor,
926
+ viewModelOf: viewModelOf,
927
+ DEFAULT_LIMIT: DEFAULT_LIMIT,
928
+ };
929
+
930
+ return module.exports;
931
+ }});