@aparte/core 0.16.9 → 0.16.10

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/dist/index.css CHANGED
@@ -332,6 +332,9 @@
332
332
  `display: none` under `(pointer: coarse)`, so 2.5.8's 24px is the bar here and not
333
333
  2.5.5's 44px. */
334
334
  --aparte-scroll-rail-hit-size: 24px;
335
+ /* The share of the transcript's height the rail may take: a list of ticks, not a
336
+ minimap, so it is the height of its list up to this, centred, and scrolls past it. */
337
+ --aparte-scroll-rail-share: .6;
335
338
 
336
339
  /* Context gauge, ring variant — a diameter, and a stroke in the ring's own 36-unit
337
340
  box. Literals like the rail's: a 22px ring is a toolbar's measure, not a spacing step. */
@@ -7265,10 +7268,10 @@ aparte-context[data-level="danger"] .aparte-context__value {
7265
7268
  * One of the sheets src/index.ts imports in cascade order; check:derived-vars reads
7266
7269
  * them concatenated in that same order, the way a browser does.
7267
7270
  *
7268
- * The rail floats on the transcript's end edge, vertically centred, as a child of the
7269
- * chat host. `:has()` gives the host its containing block only while a rail is inside
7270
- * it — a bare `position: relative` on every chat would have moved any absolutely
7271
- * positioned child a consumer already placed there.
7271
+ * The rail floats on the transcript's end edge as a child of the chat host. `:has()`
7272
+ * gives the host its containing block only while a rail is inside it — a bare
7273
+ * `position: relative` on every chat would have moved any absolutely positioned child
7274
+ * a consumer already placed there.
7272
7275
  *
7273
7276
  * The three shell shapes shell.css already lays out, because an absolute element lands
7274
7277
  * in the nearest POSITIONED ancestor and a shape missing from this list is a rail that
@@ -7284,27 +7287,57 @@ aparte-chat:has(> aparte-scroll-rail),
7284
7287
  position: relative;
7285
7288
  }
7286
7289
 
7287
- /* The block padding is the room the END zones need, and it is not decoration.
7288
- The rail clips (`overflow: hidden`, which cuts at the PADDING box) and the list has
7289
- none of its own, so with no padding here the first tick's top edge sits exactly on the
7290
- clip line. A zone grows symmetrically — half of `hit − thickness` above and below the
7291
- line — so the first tick would lose its upper half and the last tick its lower half,
7292
- to paint AND to hit-testing: 13px of the 24px, on the two ticks a reader aims at most
7293
- ("jump to the first message", "jump to the latest"). Padding by exactly that half puts
7294
- the clip line outside every zone instead of through the two end ones. `box-sizing`
7295
- keeps `max-height` measuring the same box it measured before. */
7290
+ /* Where the rail sits is measured, not assumed. The element publishes three lengths on
7291
+ itself (aparte-scroll-rail.ts, `_layout`): `--aparte-scroll-rail-bar`, the width of
7292
+ the transcript's scrollbar when it is a classic one — 0 for an overlay bar — so the
7293
+ ticks never sit on the bar under Windows or Linux Chrome; and the two block insets,
7294
+ the transcript's distance from the host's top and bottom edges, so the rail centres
7295
+ on the messages and not on the composer under them. Without a measurement (jsdom, a
7296
+ rail with no transcript yet) the fallbacks centre it on the host, where it used to be.
7297
+
7298
+ It is a LIST of ticks, not a minimap: it takes the height of its list, capped at a
7299
+ share of the transcript (`--aparte-scroll-rail-share`, .6), and sits centred on the
7300
+ transcript's span — its top is the middle of that span, pulled back by half its own
7301
+ height. Measured on the market: every per-turn rail that ships as a list caps itself
7302
+ (LobeChat at 50vh), and every full-height strip is a proportional minimap with a
7303
+ viewport window, a different component; a list stretched to the full transcript read
7304
+ as the second while behaving as the first.
7305
+
7306
+ The block padding is the room the END zones need, and it is not decoration.
7307
+ The rail clips the list at its padding box, and the list has no padding of its own,
7308
+ so with no padding here the first tick's top edge sits exactly on the clip line. A
7309
+ zone grows symmetrically — half of `hit − thickness` above and below the line — so
7310
+ the first tick would lose its upper half and the last tick its lower half, to paint
7311
+ AND to hit-testing: 13px of the 24px, on the two ticks a reader aims at most ("jump
7312
+ to the first message", "jump to the latest"). Padding by exactly that half puts the
7313
+ clip line outside every zone instead of through the two end ones. `box-sizing` keeps
7314
+ `max-height` measuring the same box it measured before.
7315
+
7316
+ When more ticks exist than 24px targets fit in the cap, the element tightens the
7317
+ pitch by setting `--aparte-scroll-rail-hit-size` on itself (never under 6px), and
7318
+ gap, padding and zone follow it here as they would a consumer's value. Past that
7319
+ floor the list still overflows, and the rail scrolls — the element keeps the current
7320
+ tick in its window, the arrows walk every tick, a wheel over the rail slides it —
7321
+ with no bar of its own: a 24px column showing a scrollbar would be all bar. */
7296
7322
  aparte-scroll-rail {
7297
7323
  position: absolute;
7298
- inset-inline-end: var(--aparte-space-2);
7299
- top: 50%;
7324
+ inset-inline-end: calc(var(--aparte-space-2) + var(--aparte-scroll-rail-bar, 0px));
7325
+ top: calc(var(--aparte-scroll-rail-block-start, 0px) + (100% - var(--aparte-scroll-rail-block-start, 0px) - var(--aparte-scroll-rail-block-end, 0px)) / 2);
7300
7326
  transform: translateY(-50%);
7301
7327
  z-index: var(--aparte-z-floating);
7302
- display: block;
7328
+ display: flex;
7329
+ flex-direction: column;
7303
7330
  box-sizing: border-box;
7304
7331
  width: var(--aparte-scroll-rail-width);
7305
7332
  padding-block: calc((var(--aparte-scroll-rail-hit-size) - var(--aparte-scroll-rail-tick-thickness)) / 2);
7306
- max-height: calc(100% - 2 * var(--aparte-space-8));
7307
- overflow: hidden;
7333
+ max-height: calc((100% - var(--aparte-scroll-rail-block-start, 0px) - var(--aparte-scroll-rail-block-end, 0px)) * var(--aparte-scroll-rail-share));
7334
+ overflow-x: hidden;
7335
+ overflow-y: auto;
7336
+ scrollbar-width: none;
7337
+ }
7338
+
7339
+ aparte-scroll-rail::-webkit-scrollbar {
7340
+ display: none;
7308
7341
  }
7309
7342
 
7310
7343
  aparte-scroll-rail[hidden],
@@ -7319,6 +7352,7 @@ aparte-scroll-rail[data-empty] {
7319
7352
  display: flex;
7320
7353
  flex-direction: column;
7321
7354
  align-items: flex-end;
7355
+ flex-shrink: 0;
7322
7356
  gap: var(--aparte-scroll-rail-gap);
7323
7357
  }
7324
7358
 
@@ -7350,10 +7384,10 @@ aparte-scroll-rail[data-empty] {
7350
7384
  Block: grow symmetrically to the hit size. The gap is `hit − thickness`, so the pitch
7351
7385
  is exactly the hit size and two zones meet without overlapping — and the rail pads its
7352
7386
  block axis by the same half-growth, so the clip does not eat the first and last zone.
7353
- Inline: grow INWARD only. The rail clips (`overflow: hidden`) and ticks are
7354
- end-aligned, so a centred zone would hang past the end edge and come back cut to
7355
- 19px; anchored to the tick's end edge it stays whole, and the rail's width carries
7356
- the hit size as its floor so the column has room for it. */
7387
+ Inline: grow INWARD only. The rail clips and ticks are end-aligned, so a centred zone
7388
+ would hang past the end edge and come back cut to 19px; anchored to the tick's end
7389
+ edge it stays whole, and the rail's width carries the hit size as its floor so the
7390
+ column has room for it. */
7357
7391
  .aparte-scroll-rail__tick::before {
7358
7392
  content: "";
7359
7393
  position: absolute;
package/dist/index.d.ts CHANGED
@@ -84,7 +84,7 @@ export { AparteConversationList } from './components/index.js';
84
84
  export type { AparteConversationListItem, AparteConversationSelectDetail, AparteConversationDeleteDetail, AparteConversationArchiveDetail, AparteConversationPinDetail, AparteConversationRenameDetail } from './components/index.js';
85
85
  export type { AparteConversation, AparteConversationMeta, AparteStorageAdapter, AparteAttachmentRow, } from './conversations/index.js';
86
86
  export { APARTE_CONVERSATION_SCHEMA_VERSION } from './conversations/index.js';
87
- export { AparteConversationManager, type ConversationManagerOptions } from './conversations/index.js';
87
+ export { AparteConversationManager, type ConversationManagerOptions, type AparteConversationTitleProvider, } from './conversations/index.js';
88
88
  export { AparteConversationController, type AparteChatBinding, type AparteConversationControllerOptions, } from './conversations/index.js';
89
89
  export { AparteChatHost, type AparteChatHostBinding, type AparteChatHostOptions, } from './host/index.js';
90
90
  export { AparteStreamParser, parseMarkdownToSegments } from './parsers/index.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { $ as resolveConfig, a4 as subscribeConfigChange, a1 as runWithConfig, o as AparteElicitationAbortError, w as buildApprovalPanel, x as buildElicitationPanel, J as escapeAttr, F as cssEscape, a7 as mergeSegmentUpdate, K as escapeHtml, a8 as writeStreamedMarkdown, C as copyText, N as getSegmentRenderer, O as installDefaultRenderersOnce, r as AparteMessageRepository, a9 as stampSegmentActivity, aa as stampSegmentOnInsert, ab as stampSegmentOnUpdate, ac as renumberSegments, ad as openSegmentIds, ae as isTerminalStatus, af as adoptMessageSegments, Q as isAwaitingReply, W as populateBubbleFromMessage, a6 as uuid, a0 as revokeAttachmentUrls, e as APARTE_DEFAULT_LOCALE, P as installDialogTriggersOnce } from "./dialog-triggers-CApC7K1I.js";
2
- import { A, a, b, c, d, f, g, h, i, j, k, l, m, n, p, q, s, t, u, v, y, z, B, D, E, G, H, I, L, M, R, S, T, U, V, X, Y, Z, _, a2, a3, a5 } from "./dialog-triggers-CApC7K1I.js";
1
+ import { $ as resolveConfig, a4 as subscribeConfigChange, a1 as runWithConfig, o as AparteElicitationAbortError, w as buildApprovalPanel, x as buildElicitationPanel, J as escapeAttr, F as cssEscape, a7 as mergeSegmentUpdate, K as escapeHtml, a8 as writeStreamedMarkdown, C as copyText, N as getSegmentRenderer, O as installDefaultRenderersOnce, r as AparteMessageRepository, a9 as stampSegmentActivity, aa as stampSegmentOnInsert, ab as stampSegmentOnUpdate, ac as renumberSegments, ad as openSegmentIds, ae as isTerminalStatus, af as adoptMessageSegments, Q as isAwaitingReply, W as populateBubbleFromMessage, a6 as uuid, a0 as revokeAttachmentUrls, e as APARTE_DEFAULT_LOCALE, P as installDialogTriggersOnce } from "./dialog-triggers-cYGTlfZf.js";
2
+ import { A, a, b, c, d, f, g, h, i, j, k, l, m, n, p, q, s, t, u, v, y, z, B, D, E, G, H, I, L, M, R, S, T, U, V, X, Y, Z, _, a2, a3, a5 } from "./dialog-triggers-cYGTlfZf.js";
3
3
  function presenceOn(value) {
4
4
  return value === "" ? true : Boolean(value);
5
5
  }
@@ -3470,11 +3470,18 @@ class AparteChatViewport extends HTMLElement {
3470
3470
  });
3471
3471
  }
3472
3472
  }
3473
- /** The overlay shell's element children that are not this viewport — the floating bottom stack. */
3473
+ /**
3474
+ * The overlay shell's element children that are the floating bottom stack: not this
3475
+ * viewport, and not the scroll rail, which floats over the transcript by design —
3476
+ * shell.css names it the same way (`:not(aparte-chat-viewport, aparte-scroll-rail)`).
3477
+ * Counted, a rail centred on the transcript set the inset to the distance from its
3478
+ * own top edge (398px on the vanilla example) and held it there while the composer
3479
+ * grew under a draft.
3480
+ */
3474
3481
  _overlayStack() {
3475
3482
  if (!this._overlayRoot) return [];
3476
3483
  return Array.from(this._overlayRoot.children).filter(
3477
- (el2) => el2 instanceof HTMLElement && el2 !== this
3484
+ (el2) => el2 instanceof HTMLElement && el2 !== this && el2.tagName.toLowerCase() !== "aparte-scroll-rail"
3478
3485
  );
3479
3486
  }
3480
3487
  /**
@@ -5882,18 +5889,40 @@ if (!customElements.get("aparte-context")) {
5882
5889
  customElements.define("aparte-context", AparteContext);
5883
5890
  }
5884
5891
  const EXCERPT_LENGTH = 60;
5892
+ const MIN_PITCH = 6;
5893
+ const SETTLE_MS = 150;
5894
+ const MAX_ALIGN_PASSES = 4;
5895
+ const ALIGN_TOLERANCE = 1;
5885
5896
  class AparteScrollRail extends HTMLElement {
5886
5897
  static get observedAttributes() {
5887
5898
  return ["target", "every"];
5888
5899
  }
5889
5900
  _host = null;
5890
5901
  _mutations = null;
5902
+ _resizes = null;
5891
5903
  _intersections = null;
5904
+ /** The bubbles the intersection observer watches — compared by identity, so a re-rendered bubble is re-observed. */
5905
+ _observedBubbles = [];
5906
+ /** The scroll surface the observer is rooted on, and whose bottom edge is read. */
5907
+ _observedSurface = null;
5892
5908
  _rebuildQueued = false;
5909
+ _pickQueued = false;
5893
5910
  _currentId = null;
5894
- /** Visible ratio per message id, from the intersection observer. */
5911
+ /** Visible ratio per message id in the reading band, from the intersection observer. */
5895
5912
  _visible = /* @__PURE__ */ new Map();
5913
+ /** The bubbles on screen at all, from the second observer; only the last one is read. */
5914
+ _onScreen = /* @__PURE__ */ new Set();
5915
+ _onScreenObserver = null;
5916
+ /** The mark a jump asked for, held until the transcript has stopped moving. */
5917
+ _held = null;
5918
+ _settleTimer = null;
5919
+ _settleSurface = null;
5920
+ /** Re-alignments done for the jump in flight. */
5921
+ _alignPasses = 0;
5922
+ /** The custom properties written on this element, so each is written only when it changes. */
5923
+ _written = /* @__PURE__ */ new Map();
5896
5924
  _onConfigChange = () => this._rebuild();
5925
+ _onSurfaceScroll = () => this._bumpSettle();
5897
5926
  /** The id of the message the rail currently marks, or `null`. */
5898
5927
  get currentMessageId() {
5899
5928
  return this._currentId;
@@ -5937,17 +5966,26 @@ class AparteScrollRail extends HTMLElement {
5937
5966
  return;
5938
5967
  }
5939
5968
  if (typeof MutationObserver !== "undefined") {
5940
- this._mutations = new MutationObserver(() => this._queueRebuild());
5969
+ this._mutations = new MutationObserver((records) => {
5970
+ if (records.some((record) => !this.contains(record.target))) this._queueRebuild();
5971
+ });
5941
5972
  this._mutations.observe(this._host, { childList: true, subtree: true, characterData: true });
5942
5973
  }
5974
+ if (typeof ResizeObserver !== "undefined") {
5975
+ this._resizes = new ResizeObserver(() => this._layout());
5976
+ this._resizes.observe(this._host);
5977
+ const viewport = this._viewport();
5978
+ if (viewport) this._resizes.observe(viewport);
5979
+ }
5943
5980
  this._rebuild();
5944
5981
  }
5945
5982
  _detach() {
5946
5983
  this._mutations?.disconnect();
5947
5984
  this._mutations = null;
5948
- this._intersections?.disconnect();
5949
- this._intersections = null;
5950
- this._visible.clear();
5985
+ this._resizes?.disconnect();
5986
+ this._resizes = null;
5987
+ this._unobserve();
5988
+ this._release();
5951
5989
  this._host = null;
5952
5990
  }
5953
5991
  /** Coalesce a burst of mutations (a streaming reply is one per token) into one rebuild. */
@@ -5988,6 +6026,15 @@ class AparteScrollRail extends HTMLElement {
5988
6026
  const excerpt = this._excerpt(bubble);
5989
6027
  return excerpt ? `${role}: ${excerpt}` : role;
5990
6028
  }
6029
+ _list() {
6030
+ return this.querySelector(":scope > .aparte-scroll-rail__list");
6031
+ }
6032
+ /**
6033
+ * Reconcile the ticks with the bubbles, by message id. A tick that already exists is
6034
+ * kept — the same node, so the focus on it, its hover and its tooltip survive a turn
6035
+ * being appended or a reply streaming beside it — moved only when its order changed,
6036
+ * and dropped when its bubble left.
6037
+ */
5991
6038
  _rebuild() {
5992
6039
  const bubbles = this._bubbles();
5993
6040
  const locale = resolveConfig(this).getLocale();
@@ -5995,45 +6042,71 @@ class AparteScrollRail extends HTMLElement {
5995
6042
  if (bubbles.length < 2) {
5996
6043
  this.setAttribute("data-empty", "");
5997
6044
  this.replaceChildren();
5998
- this._intersections?.disconnect();
5999
- this._intersections = null;
6000
- this._visible.clear();
6045
+ this._unobserve();
6001
6046
  this._currentId = null;
6047
+ this._write("--aparte-scroll-rail-hit-size", "");
6002
6048
  return;
6003
6049
  }
6004
6050
  this.removeAttribute("data-empty");
6005
- const list = document.createElement("ol");
6006
- list.className = "aparte-scroll-rail__list";
6051
+ let list = this._list();
6052
+ if (!list) {
6053
+ list = document.createElement("ol");
6054
+ list.className = "aparte-scroll-rail__list";
6055
+ this.replaceChildren(list);
6056
+ }
6057
+ const existing = /* @__PURE__ */ new Map();
6058
+ for (const item of Array.from(list.children)) {
6059
+ const id = item.firstElementChild instanceof HTMLElement ? item.firstElementChild.dataset["messageId"] : void 0;
6060
+ if (id) existing.set(id, item);
6061
+ }
6007
6062
  const known = /* @__PURE__ */ new Set();
6063
+ let index = 0;
6008
6064
  for (const bubble of bubbles) {
6009
6065
  const id = bubble.getAttribute("message-id");
6010
6066
  known.add(id);
6011
- const item = document.createElement("li");
6012
- const tick = document.createElement("button");
6013
- tick.type = "button";
6014
- tick.className = "aparte-scroll-rail__tick";
6015
- tick.dataset["messageId"] = id;
6067
+ let item = existing.get(id);
6068
+ if (item) {
6069
+ existing.delete(id);
6070
+ } else {
6071
+ item = document.createElement("li");
6072
+ const tick2 = document.createElement("button");
6073
+ tick2.type = "button";
6074
+ tick2.className = "aparte-scroll-rail__tick";
6075
+ tick2.dataset["messageId"] = id;
6076
+ item.appendChild(tick2);
6077
+ }
6078
+ const tick = item.firstElementChild;
6016
6079
  const label = this._tickLabel(bubble);
6017
- tick.setAttribute("aria-label", label);
6018
- tick.title = label;
6080
+ if (tick.getAttribute("aria-label") !== label) {
6081
+ tick.setAttribute("aria-label", label);
6082
+ tick.title = label;
6083
+ }
6019
6084
  if (id === this._currentId) tick.setAttribute("aria-current", "true");
6020
- item.appendChild(tick);
6021
- list.appendChild(item);
6085
+ else tick.removeAttribute("aria-current");
6086
+ const at = list.children[index] ?? null;
6087
+ if (at !== item) list.insertBefore(item, at);
6088
+ index++;
6022
6089
  }
6023
- this.replaceChildren(list);
6090
+ for (const stale of existing.values()) stale.remove();
6024
6091
  if (this._currentId && !known.has(this._currentId)) this._currentId = null;
6025
- this._observe(bubbles);
6092
+ if (!sameElements(this._observedBubbles, bubbles)) this._observe(bubbles);
6093
+ this._layout();
6026
6094
  }
6027
6095
  /**
6028
- * Which tick is current is read, not computed: the scroll surface is the root, and
6029
- * the bubble with the largest visible share of a reading band near the top wins.
6030
- * Scroll arithmetic was the alternative, and every scroll-position rule in the
6031
- * viewport carries a regression note — this one has none to carry.
6096
+ * Which tick is current is read, not computed: the scroll surface is the root and an
6097
+ * intersection observer reports which bubbles reach a reading band — the top 30% of
6098
+ * the surface, from its very top so that the bubble a jump aligned there counts. The
6099
+ * rule is a scrollspy's: the LAST question that has reached the band is the one the
6100
+ * reader is under. "The largest visible share" was the rule before, and at the bottom
6101
+ * of a thread of short turns, where three questions sit in the band, it named the
6102
+ * first of them. Scroll arithmetic was the alternative, and every scroll-position
6103
+ * rule in the viewport carries a regression note; the one read here is the bottom,
6104
+ * because at the bottom the reader is under the latest question whatever the band
6105
+ * holds, and the observer has nothing to report on the last few pixels.
6032
6106
  */
6033
6107
  _observe(bubbles) {
6034
- this._intersections?.disconnect();
6035
- this._intersections = null;
6036
- this._visible.clear();
6108
+ this._unobserve();
6109
+ this._observedBubbles = bubbles;
6037
6110
  if (typeof IntersectionObserver === "undefined") return;
6038
6111
  const root = this._scrollSurface();
6039
6112
  if (!root) return;
@@ -6042,30 +6115,142 @@ class AparteScrollRail extends HTMLElement {
6042
6115
  const id = entry.target.getAttribute("message-id");
6043
6116
  if (id) this._visible.set(id, entry.isIntersecting ? entry.intersectionRatio : 0);
6044
6117
  }
6045
- this._pickCurrent(bubbles);
6046
- }, { root, rootMargin: "-15% 0px -55% 0px", threshold: [0, 0.1, 0.25, 0.5, 0.75, 1] });
6047
- for (const bubble of bubbles) this._intersections.observe(bubble);
6118
+ this._pickCurrent();
6119
+ }, { root, rootMargin: "0px 0px -70% 0px", threshold: [0, 0.1, 0.25, 0.5, 0.75, 1] });
6120
+ this._onScreenObserver = new IntersectionObserver((entries) => {
6121
+ for (const entry of entries) {
6122
+ const id = entry.target.getAttribute("message-id");
6123
+ if (!id) continue;
6124
+ if (entry.isIntersecting) this._onScreen.add(id);
6125
+ else this._onScreen.delete(id);
6126
+ }
6127
+ this._pickCurrent();
6128
+ }, { root, threshold: [0] });
6129
+ for (const bubble of bubbles) {
6130
+ this._intersections.observe(bubble);
6131
+ this._onScreenObserver.observe(bubble);
6132
+ }
6133
+ this._observedSurface = root;
6134
+ root.addEventListener("scroll", this._onRootScroll, { passive: true });
6048
6135
  }
6049
- _pickCurrent(bubbles) {
6050
- let best = null;
6051
- let bestRatio = 0;
6136
+ _unobserve() {
6137
+ this._intersections?.disconnect();
6138
+ this._intersections = null;
6139
+ this._onScreenObserver?.disconnect();
6140
+ this._onScreenObserver = null;
6141
+ this._observedSurface?.removeEventListener("scroll", this._onRootScroll);
6142
+ this._observedSurface = null;
6143
+ this._observedBubbles = [];
6144
+ this._visible.clear();
6145
+ this._onScreen.clear();
6146
+ }
6147
+ /** A scroll only matters at the bottom edge, where the observer is silent: one pick per frame. */
6148
+ _onRootScroll = () => {
6149
+ if (this._pickQueued) return;
6150
+ this._pickQueued = true;
6151
+ requestAnimationFrame(() => {
6152
+ this._pickQueued = false;
6153
+ this._pickCurrent();
6154
+ });
6155
+ };
6156
+ /** At the bottom edge of a surface that scrolls; a surface with nothing to scroll has no bottom to be at. */
6157
+ _atBottom() {
6158
+ const surface = this._observedSurface;
6159
+ if (!surface) return false;
6160
+ const max = surface.scrollHeight - surface.clientHeight;
6161
+ return max > 0 && max - surface.scrollTop <= 2;
6162
+ }
6163
+ _pickCurrent() {
6164
+ if (this._held) return;
6165
+ const bubbles = this._observedBubbles;
6166
+ if (bubbles.length === 0) return;
6167
+ const lastId = bubbles[bubbles.length - 1].getAttribute("message-id");
6168
+ if (this._atBottom() || this._onScreen.has(lastId)) {
6169
+ this._setCurrent(lastId);
6170
+ return;
6171
+ }
6172
+ let last = null;
6052
6173
  for (const bubble of bubbles) {
6053
6174
  const id = bubble.getAttribute("message-id");
6054
- const ratio = this._visible.get(id) ?? 0;
6055
- if (ratio > bestRatio) {
6056
- bestRatio = ratio;
6057
- best = id;
6058
- }
6175
+ if ((this._visible.get(id) ?? 0) > 0) last = id;
6059
6176
  }
6060
- if (best !== null) this._setCurrent(best);
6177
+ if (last !== null) this._setCurrent(last);
6061
6178
  }
6062
6179
  _setCurrent(id) {
6063
6180
  if (this._currentId === id) return;
6064
6181
  this._currentId = id;
6065
6182
  for (const tick of this.querySelectorAll(".aparte-scroll-rail__tick")) {
6066
- if (tick.dataset["messageId"] === id) tick.setAttribute("aria-current", "true");
6067
- else tick.removeAttribute("aria-current");
6183
+ if (tick.dataset["messageId"] === id) {
6184
+ tick.setAttribute("aria-current", "true");
6185
+ this._reveal(tick);
6186
+ } else {
6187
+ tick.removeAttribute("aria-current");
6188
+ }
6189
+ }
6190
+ }
6191
+ /** Past the floor the rail scrolls itself: the current tick is kept inside its window. */
6192
+ _reveal(tick) {
6193
+ const room = this.clientHeight;
6194
+ if (!room) return;
6195
+ const top = tick.offsetTop;
6196
+ const bottom = top + tick.offsetHeight;
6197
+ if (top < this.scrollTop) this.scrollTop = top;
6198
+ else if (bottom > this.scrollTop + room) this.scrollTop = bottom - room;
6199
+ }
6200
+ // ─── Geometry ─────────────────────────────────────────────────────────
6201
+ /** Write a custom property on this element only when its value changed; '' removes it. */
6202
+ _write(name, value) {
6203
+ if (this._written.get(name) === value) return;
6204
+ this._written.set(name, value);
6205
+ if (value) this.style.setProperty(name, value);
6206
+ else this.style.removeProperty(name);
6207
+ }
6208
+ /**
6209
+ * The three measurements the stylesheet positions the rail by, and the pitch.
6210
+ *
6211
+ * The scrollbar: `offsetWidth − clientWidth` of the scroll surface is the width of a
6212
+ * classic bar and 0 for an overlay one, so the rail sits clear of the bar exactly
6213
+ * where there is one. The extent: the transcript's box within the host, so the rail
6214
+ * spans the messages and not the composer under them. The pitch: the stylesheet's
6215
+ * 24px targets whenever `count × 24` fits the rail; otherwise the pitch that does,
6216
+ * never under `MIN_PITCH` — the rail sets `--aparte-scroll-rail-hit-size` on itself
6217
+ * and the gap, the padding and the zone all follow it, as they do for a consumer's
6218
+ * value. Past the floor the rail scrolls (`_reveal`), and the arrows still walk it.
6219
+ */
6220
+ _layout() {
6221
+ const host = this._host;
6222
+ const viewport = this._viewport();
6223
+ const surface = this._scrollSurface();
6224
+ if (host && surface) {
6225
+ this._write("--aparte-scroll-rail-bar", `${Math.max(0, surface.offsetWidth - surface.clientWidth)}px`);
6226
+ }
6227
+ if (host && viewport) {
6228
+ const v2 = viewport.getBoundingClientRect();
6229
+ const h2 = host.getBoundingClientRect();
6230
+ if (v2.height > 0 && h2.height > 0) {
6231
+ this._write("--aparte-scroll-rail-block-start", `${Math.max(0, Math.round(v2.top - h2.top))}px`);
6232
+ this._write("--aparte-scroll-rail-block-end", `${Math.max(0, Math.round(h2.bottom - v2.bottom))}px`);
6233
+ }
6234
+ }
6235
+ const count = this._list()?.children.length ?? 0;
6236
+ const height = this.clientHeight;
6237
+ if (!count || !height) {
6238
+ this._write("--aparte-scroll-rail-hit-size", "");
6239
+ this._write("--aparte-scroll-rail-gap", "");
6240
+ return;
6241
+ }
6242
+ const above = host ?? document.documentElement;
6243
+ const aboveStyle = getComputedStyle(above);
6244
+ const hit = parseFloat(aboveStyle.getPropertyValue("--aparte-scroll-rail-hit-size")) || 24;
6245
+ const thickness = parseFloat(aboveStyle.getPropertyValue("--aparte-scroll-rail-tick-thickness")) || 2;
6246
+ if (count * hit <= height) {
6247
+ this._write("--aparte-scroll-rail-hit-size", "");
6248
+ this._write("--aparte-scroll-rail-gap", "");
6249
+ return;
6068
6250
  }
6251
+ const pitch = Math.max(MIN_PITCH, Math.floor(height / count));
6252
+ this._write("--aparte-scroll-rail-hit-size", `${pitch}px`);
6253
+ this._write("--aparte-scroll-rail-gap", `${Math.max(0, pitch - thickness)}px`);
6069
6254
  }
6070
6255
  // ─── Interaction ──────────────────────────────────────────────────────
6071
6256
  _onClick = (e) => {
@@ -6088,6 +6273,16 @@ class AparteScrollRail extends HTMLElement {
6088
6273
  /**
6089
6274
  * Scroll the transcript to a message. Announced first, cancelable; then a
6090
6275
  * `scrollIntoView` on the bubble, smooth unless the reader prefers reduced motion.
6276
+ * The clicked tick is the mark until the transcript has stopped moving: while a
6277
+ * smooth scroll crosses other questions the band would name each in turn, and it
6278
+ * ended on the wrong one two times in three before this hold.
6279
+ *
6280
+ * Where the scroll lands is checked once it has settled, and corrected. The bubbles
6281
+ * carry `content-visibility: auto`, so a bubble far from the reader has an estimated
6282
+ * height until it is rendered: the scroll aims at an estimate, the bubbles above it
6283
+ * take their real size on the way, and the message ends up above or below the top —
6284
+ * measured from 36px to 1,213px off, in all three engines, the further the worse.
6285
+ * A second `scrollIntoView` from a rendered neighbourhood lands; a few are allowed.
6091
6286
  */
6092
6287
  jumpTo(messageId) {
6093
6288
  const announced = this.dispatchEvent(new CustomEvent("aparte-scroll-rail-jump", {
@@ -6097,13 +6292,61 @@ class AparteScrollRail extends HTMLElement {
6097
6292
  cancelable: true
6098
6293
  }));
6099
6294
  if (!announced) return;
6100
- const viewport = this._viewport();
6101
- const bubble = viewport?.querySelector(`aparte-chat-bubble[message-id="${cssEscape(messageId)}"]`);
6295
+ const bubble = this._bubbleOf(messageId);
6102
6296
  if (!bubble) return;
6297
+ this._scrollTo(bubble);
6298
+ this._setCurrent(messageId);
6299
+ this._hold(messageId);
6300
+ }
6301
+ _bubbleOf(messageId) {
6302
+ return this._viewport()?.querySelector(`aparte-chat-bubble[message-id="${cssEscape(messageId)}"]`) ?? null;
6303
+ }
6304
+ _scrollTo(bubble) {
6103
6305
  const reduced = typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
6104
6306
  bubble.scrollIntoView({ block: "start", behavior: reduced ? "auto" : "smooth" });
6105
- this._setCurrent(messageId);
6106
6307
  }
6308
+ /** Keep `id` as the mark until the scroll surface has been still for `SETTLE_MS`. */
6309
+ _hold(id) {
6310
+ this._release();
6311
+ this._held = id;
6312
+ this._alignPasses = 0;
6313
+ this._settleSurface = this._scrollSurface();
6314
+ this._settleSurface?.addEventListener("scroll", this._onSurfaceScroll);
6315
+ this._bumpSettle();
6316
+ }
6317
+ _bumpSettle() {
6318
+ if (this._settleTimer !== null) clearTimeout(this._settleTimer);
6319
+ this._settleTimer = setTimeout(() => this._settle(), SETTLE_MS);
6320
+ }
6321
+ /** The transcript is still: land on the message if the scroll did not, else release the hold. */
6322
+ _settle() {
6323
+ const id = this._held;
6324
+ const surface = this._settleSurface;
6325
+ const bubble = id ? this._bubbleOf(id) : null;
6326
+ if (id && bubble && surface && this._alignPasses < MAX_ALIGN_PASSES) {
6327
+ const off = bubble.getBoundingClientRect().top - surface.getBoundingClientRect().top;
6328
+ if (Math.abs(off) > ALIGN_TOLERANCE) {
6329
+ this._alignPasses++;
6330
+ this._scrollTo(bubble);
6331
+ this._bumpSettle();
6332
+ return;
6333
+ }
6334
+ }
6335
+ this._release();
6336
+ if (id) this._setCurrent(id);
6337
+ }
6338
+ _release() {
6339
+ if (this._settleTimer !== null) clearTimeout(this._settleTimer);
6340
+ this._settleTimer = null;
6341
+ this._settleSurface?.removeEventListener("scroll", this._onSurfaceScroll);
6342
+ this._settleSurface = null;
6343
+ this._held = null;
6344
+ }
6345
+ }
6346
+ function sameElements(a4, b2) {
6347
+ if (a4.length !== b2.length) return false;
6348
+ for (let i2 = 0; i2 < a4.length; i2++) if (a4[i2] !== b2[i2]) return false;
6349
+ return true;
6107
6350
  }
6108
6351
  if (!customElements.get("aparte-scroll-rail")) {
6109
6352
  customElements.define("aparte-scroll-rail", AparteScrollRail);