@khorsheed/dsh-message-timeline 0.2.7 → 0.2.8

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 CHANGED
@@ -1,5 +1,9 @@
1
1
  # 变更记录
2
2
 
3
+ ## 0.2.8(2026-10-27)
4
+
5
+ - **修复会话底部的消息点击后无法选中**:点击最新一条或底部可视区内的后续消息时,浏览器可能已把滚动位置钳制在最大值,原先高亮因此回退到第一条可见消息。现在跳转会保留实际点击落点;真正滚动、目标行移除或会话重绑后再恢复普通阅读位置跟踪。
6
+
3
7
  ## 0.2.7(2026-10-26)
4
8
 
5
9
  - **README 重写**:改为面向用户的简短说明。无功能变更。
package/lib/client.js CHANGED
@@ -201,18 +201,12 @@ window.__ModuleLoader__.load({
201
201
  }
202
202
  return lastAbove;
203
203
  }
204
- /**
205
- * Scroll one user-message row to the top of the transcript scrollport.
206
- * @param scrollport - the official conversation scrollport element.
207
- * @param key - the target node's anchor key.
208
- * @returns whether the row was found and scrolled.
209
- */
210
- function jumpRow(scrollport, key) {
204
+ function jumpTarget(scrollport, key) {
211
205
  const row = scrollport.querySelector(`[data-chat-anchor-key=${JSON.stringify(key)}]`);
212
- if (row === null) return false;
206
+ if (row === null) return null;
213
207
  const target = row.getBoundingClientRect().top - scrollport.getBoundingClientRect().top + scrollport.scrollTop - JUMP_OFFSET;
214
208
  scrollport.scrollTop = Math.max(0, target);
215
- return true;
209
+ return row;
216
210
  }
217
211
  /**
218
212
  * Install the rail tracker: bind the current session's scrollport, publish
@@ -228,6 +222,7 @@ window.__ModuleLoader__.load({
228
222
  let state = IDLE;
229
223
  let activeSession;
230
224
  let scrollport = null;
225
+ let landing = null;
231
226
  let resizeObserver;
232
227
  let rafPending = false;
233
228
  let warned = false;
@@ -244,6 +239,7 @@ window.__ModuleLoader__.load({
244
239
  const attach = (el) => {
245
240
  if (scrollport !== null) scrollport.removeEventListener("scroll", onScroll);
246
241
  resizeObserver?.disconnect();
242
+ landing = null;
247
243
  scrollport = el;
248
244
  scrollport.addEventListener("scroll", onScroll, { passive: true });
249
245
  if (typeof ResizeObserver === "function") {
@@ -264,6 +260,7 @@ window.__ModuleLoader__.load({
264
260
  }
265
261
  const geometry = measureGeometry(el);
266
262
  if (geometry === null) return;
263
+ if (landing !== null && (landing.top !== el.scrollTop || !el.contains(landing.row))) landing = null;
267
264
  publish({
268
265
  sessionId: activeSession,
269
266
  ready: true,
@@ -272,7 +269,7 @@ window.__ModuleLoader__.load({
272
269
  height: geometry.height,
273
270
  scrollportWidth: geometry.width,
274
271
  flowLeft: flowLeftX(el),
275
- activeKey: activeRowKey(el, includeSteering),
272
+ activeKey: landing?.key ?? activeRowKey(el, includeSteering),
276
273
  chatView: el.querySelector("[data-chat-flow]") !== null
277
274
  });
278
275
  };
@@ -291,6 +288,7 @@ window.__ModuleLoader__.load({
291
288
  bindToken++;
292
289
  if (scrollport !== null) scrollport.removeEventListener("scroll", onScroll);
293
290
  scrollport = null;
291
+ landing = null;
294
292
  resizeObserver?.disconnect();
295
293
  resizeObserver = void 0;
296
294
  rafPending = false;
@@ -347,7 +345,22 @@ window.__ModuleLoader__.load({
347
345
  };
348
346
  }
349
347
  },
350
- jumpTo: (key) => scrollport !== null && jumpRow(scrollport, key),
348
+ jumpTo: (key) => {
349
+ const el = scrollport;
350
+ if (el === null) return false;
351
+ const row = jumpTarget(el, key);
352
+ if (row === null) return false;
353
+ landing = {
354
+ key,
355
+ row,
356
+ top: el.scrollTop
357
+ };
358
+ publish({
359
+ ...state,
360
+ activeKey: key
361
+ });
362
+ return true;
363
+ },
351
364
  dispose: () => {
352
365
  stopList();
353
366
  stopProvide?.();
@@ -561,16 +574,16 @@ window.__ModuleLoader__.load({
561
574
  document.head.appendChild(tag);
562
575
  }
563
576
  var TimelineRail_module_css_default = {
564
- "itemFocused": "_8u17xW_itemFocused",
565
- "itemText": "_8u17xW_itemText",
566
- "railPending": "_8u17xW_railPending",
567
- "tick": "_8u17xW_tick",
568
577
  "panel": "_8u17xW_panel",
569
- "itemCurrent": "_8u17xW_itemCurrent",
570
578
  "itemPending": "_8u17xW_itemPending",
579
+ "railPending": "_8u17xW_railPending",
580
+ "item": "_8u17xW_item",
571
581
  "panelNarrow": "_8u17xW_panelNarrow",
582
+ "tick": "_8u17xW_tick",
583
+ "itemCurrent": "_8u17xW_itemCurrent",
572
584
  "tickCurrent": "_8u17xW_tickCurrent",
573
- "item": "_8u17xW_item"
585
+ "itemText": "_8u17xW_itemText",
586
+ "itemFocused": "_8u17xW_itemFocused"
574
587
  };
575
588
  //#endregion
576
589
  //#region src/client/TimelineRail.tsx
@@ -79,8 +79,9 @@ export interface RailTracker {
79
79
  /** Live rail geometry and active marker for the current session. */
80
80
  readonly state: HostObservable<TimelineRailState>;
81
81
  /**
82
- * Scroll the transcript to the message addressed by `key`.
83
- * @returns whether the row was found and scrolled (false while unbound).
82
+ * Scroll to the message addressed by `key` and keep that landing active
83
+ * until the reading position moves or the row disappears.
84
+ * @returns whether the target row was found (false while unbound or missing).
84
85
  */
85
86
  jumpTo(key: string): boolean;
86
87
  /** Unbind every listener and observer. */
@@ -131,6 +131,15 @@ export function activeRowKey(scrollport, includeSteering) {
131
131
  }
132
132
  return lastAbove;
133
133
  }
134
+ function jumpTarget(scrollport, key) {
135
+ const row = scrollport.querySelector(`[data-chat-anchor-key=${JSON.stringify(key)}]`);
136
+ if (row === null)
137
+ return null;
138
+ const target = row.getBoundingClientRect().top - scrollport.getBoundingClientRect().top
139
+ + scrollport.scrollTop - JUMP_OFFSET;
140
+ scrollport.scrollTop = Math.max(0, target);
141
+ return row;
142
+ }
134
143
  /**
135
144
  * Scroll one user-message row to the top of the transcript scrollport.
136
145
  * @param scrollport - the official conversation scrollport element.
@@ -138,13 +147,7 @@ export function activeRowKey(scrollport, includeSteering) {
138
147
  * @returns whether the row was found and scrolled.
139
148
  */
140
149
  export function jumpRow(scrollport, key) {
141
- const row = scrollport.querySelector(`[data-chat-anchor-key=${JSON.stringify(key)}]`);
142
- if (row === null)
143
- return false;
144
- const target = row.getBoundingClientRect().top - scrollport.getBoundingClientRect().top
145
- + scrollport.scrollTop - JUMP_OFFSET;
146
- scrollport.scrollTop = Math.max(0, target);
147
- return true;
150
+ return jumpTarget(scrollport, key) !== null;
148
151
  }
149
152
  /**
150
153
  * Install the rail tracker: bind the current session's scrollport, publish
@@ -160,6 +163,7 @@ export function installRailTracker(ctx, includeSteering) {
160
163
  let state = IDLE;
161
164
  let activeSession;
162
165
  let scrollport = null;
166
+ let landing = null;
163
167
  let resizeObserver;
164
168
  let rafPending = false;
165
169
  let warned = false;
@@ -189,6 +193,7 @@ export function installRailTracker(ctx, includeSteering) {
189
193
  if (scrollport !== null)
190
194
  scrollport.removeEventListener('scroll', onScroll);
191
195
  resizeObserver?.disconnect();
196
+ landing = null;
192
197
  scrollport = el;
193
198
  scrollport.addEventListener('scroll', onScroll, { passive: true });
194
199
  if (typeof ResizeObserver === 'function') {
@@ -218,6 +223,8 @@ export function installRailTracker(ctx, includeSteering) {
218
223
  const geometry = measureGeometry(el);
219
224
  if (geometry === null)
220
225
  return;
226
+ if (landing !== null && (landing.top !== el.scrollTop || !el.contains(landing.row)))
227
+ landing = null;
221
228
  publish({
222
229
  sessionId: activeSession,
223
230
  ready: true,
@@ -226,7 +233,7 @@ export function installRailTracker(ctx, includeSteering) {
226
233
  height: geometry.height,
227
234
  scrollportWidth: geometry.width,
228
235
  flowLeft: flowLeftX(el),
229
- activeKey: activeRowKey(el, includeSteering),
236
+ activeKey: landing?.key ?? activeRowKey(el, includeSteering),
230
237
  chatView: el.querySelector('[data-chat-flow]') !== null,
231
238
  });
232
239
  };
@@ -245,6 +252,7 @@ export function installRailTracker(ctx, includeSteering) {
245
252
  if (scrollport !== null)
246
253
  scrollport.removeEventListener('scroll', onScroll);
247
254
  scrollport = null;
255
+ landing = null;
248
256
  resizeObserver?.disconnect();
249
257
  resizeObserver = undefined;
250
258
  rafPending = false;
@@ -314,7 +322,17 @@ export function installRailTracker(ctx, includeSteering) {
314
322
  return () => { listeners.delete(fn); };
315
323
  },
316
324
  },
317
- jumpTo: (key) => scrollport !== null && jumpRow(scrollport, key),
325
+ jumpTo: (key) => {
326
+ const el = scrollport;
327
+ if (el === null)
328
+ return false;
329
+ const row = jumpTarget(el, key);
330
+ if (row === null)
331
+ return false;
332
+ landing = { key, row, top: el.scrollTop };
333
+ publish({ ...state, activeKey: key });
334
+ return true;
335
+ },
318
336
  dispose: () => {
319
337
  stopList();
320
338
  stopProvide?.();
@@ -58,8 +58,9 @@ export interface TimelineRailInjected {
58
58
  */
59
59
  loadThrough: ISession['loadThrough'];
60
60
  /**
61
- * Scroll the transcript to the user message row addressed by `key`.
62
- * @returns whether the row was found and scrolled.
61
+ * Scroll to the user message addressed by `key` and keep that landing active
62
+ * until the reading position moves or the row disappears.
63
+ * @returns whether the target row was found (false while unbound or missing).
63
64
  */
64
65
  jumpTo: (key: string) => boolean;
65
66
  hooks: {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@khorsheed/dsh-message-timeline",
3
3
  "description": "Message timeline: a flat floating list over the conversation scrollport that jumps to any user message, with a tick-only rest state",
4
- "version": "0.2.7",
4
+ "version": "0.2.8",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "types": "lib/types/index.d.ts",