@djangocfg/ui-tools 2.1.445 → 2.1.446
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/ui-tools",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.446",
|
|
4
4
|
"description": "Heavy React tools with lazy loading - for Electron, Vite, CRA, Next.js apps",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ui-tools",
|
|
@@ -334,8 +334,8 @@
|
|
|
334
334
|
"test:watch": "vitest"
|
|
335
335
|
},
|
|
336
336
|
"peerDependencies": {
|
|
337
|
-
"@djangocfg/i18n": "^2.1.
|
|
338
|
-
"@djangocfg/ui-core": "^2.1.
|
|
337
|
+
"@djangocfg/i18n": "^2.1.446",
|
|
338
|
+
"@djangocfg/ui-core": "^2.1.446",
|
|
339
339
|
"consola": "^3.4.2",
|
|
340
340
|
"lodash-es": "^4.18.1",
|
|
341
341
|
"lucide-react": "^0.545.0",
|
|
@@ -418,9 +418,9 @@
|
|
|
418
418
|
"@maplibre/maplibre-gl-geocoder": "^1.7.0"
|
|
419
419
|
},
|
|
420
420
|
"devDependencies": {
|
|
421
|
-
"@djangocfg/i18n": "^2.1.
|
|
422
|
-
"@djangocfg/typescript-config": "^2.1.
|
|
423
|
-
"@djangocfg/ui-core": "^2.1.
|
|
421
|
+
"@djangocfg/i18n": "^2.1.446",
|
|
422
|
+
"@djangocfg/typescript-config": "^2.1.446",
|
|
423
|
+
"@djangocfg/ui-core": "^2.1.446",
|
|
424
424
|
"@types/lodash-es": "^4.17.12",
|
|
425
425
|
"@types/mapbox__mapbox-gl-draw": "^1.4.8",
|
|
426
426
|
"@types/node": "^25.2.3",
|
|
@@ -622,7 +622,10 @@ export const Composer = forwardRef<HTMLDivElement, ComposerProps>(function Compo
|
|
|
622
622
|
{...dragProps}
|
|
623
623
|
onKeyDownCapture={handleSlashKeyDownCapture}
|
|
624
624
|
className={cn(
|
|
625
|
-
|
|
625
|
+
// No border-t: the composer's own rounded field separates it from
|
|
626
|
+
// the transcript — a hard rule above the input reads as chrome
|
|
627
|
+
// (ChatGPT/Claude float the composer the same way).
|
|
628
|
+
'relative bg-background/95',
|
|
626
629
|
sz.containerPadding,
|
|
627
630
|
ap.containerPadding,
|
|
628
631
|
className,
|
|
@@ -744,7 +747,9 @@ export const Composer = forwardRef<HTMLDivElement, ComposerProps>(function Compo
|
|
|
744
747
|
{...dragProps}
|
|
745
748
|
onKeyDownCapture={handleSlashKeyDownCapture}
|
|
746
749
|
className={cn(
|
|
747
|
-
|
|
750
|
+
// No border-t: the composer's own rounded field separates it from
|
|
751
|
+
// the transcript (same rationale as the inline layout above).
|
|
752
|
+
'relative bg-background/95',
|
|
748
753
|
sz.containerPadding,
|
|
749
754
|
ap.containerPadding,
|
|
750
755
|
className,
|
|
@@ -191,23 +191,84 @@ export const MessageList = forwardRef<MessageListHandle, MessageListProps>(funct
|
|
|
191
191
|
);
|
|
192
192
|
|
|
193
193
|
const itemRenderer = renderItem ?? defaultRenderItem;
|
|
194
|
-
// Jump to bottom on the first non-empty messages batch.
|
|
195
|
-
//
|
|
196
|
-
//
|
|
197
|
-
//
|
|
194
|
+
// Jump to bottom on the first non-empty messages batch. A SINGLE rAF +
|
|
195
|
+
// scrollToIndex is not enough when the list mounts with a long transcript
|
|
196
|
+
// (opening / switching a contact): virtuoso measures the dynamic bubble
|
|
197
|
+
// heights ASYNCHRONOUSLY, seconds after the data lands — until then the
|
|
198
|
+
// scroller reports scrollHeight === clientHeight (gap 0), so any "stop when
|
|
199
|
+
// the bottom sticks" exit fires on that transient zero and the late height
|
|
200
|
+
// growth is never corrected (live incident 2026-07-03: contact switch left
|
|
201
|
+
// a 112-message room at scrollTop 0 / blank until measured). The landing is
|
|
202
|
+
// therefore a PHASE, not a one-shot: ONE scrollToIndex seeds the tail
|
|
203
|
+
// render, then per-frame DIRECT scrollTop writes chase every height change
|
|
204
|
+
// for the whole landing window (~2.5s) — ending early ONLY on real user
|
|
205
|
+
// scroll intent, never on a momentarily-zero gap. Direct writes are
|
|
206
|
+
// ordinary scrolls to virtuoso — repeating scrollToIndex instead re-enters
|
|
207
|
+
// its initial-location seek and blanks the list (zero rows,
|
|
208
|
+
// visibility:hidden).
|
|
198
209
|
useEffect(() => {
|
|
199
210
|
if (didInitialScrollRef.current) return;
|
|
200
211
|
if (messages.length === 0) return;
|
|
201
212
|
didInitialScrollRef.current = true;
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
213
|
+
let raf = 0;
|
|
214
|
+
let framesLeft = 150;
|
|
215
|
+
let seeded = false;
|
|
216
|
+
let userTookOver = false;
|
|
217
|
+
const cancelOnUserScroll = () => {
|
|
218
|
+
userTookOver = true;
|
|
219
|
+
};
|
|
220
|
+
// Real scroll intent (wheel / touch / keys) ends the landing phase — the
|
|
221
|
+
// user reading history must never be yanked back down. Programmatic
|
|
222
|
+
// scrollTop writes fire none of these, so our own corrections don't
|
|
223
|
+
// self-cancel.
|
|
224
|
+
const scroller = scrollerRef.current;
|
|
225
|
+
const scrollerEl =
|
|
226
|
+
scroller != null && scroller !== window && scroller instanceof HTMLElement
|
|
227
|
+
? scroller
|
|
228
|
+
: null;
|
|
229
|
+
scrollerEl?.addEventListener('wheel', cancelOnUserScroll, { passive: true });
|
|
230
|
+
scrollerEl?.addEventListener('touchstart', cancelOnUserScroll, { passive: true });
|
|
231
|
+
scrollerEl?.addEventListener('keydown', cancelOnUserScroll);
|
|
232
|
+
const land = () => {
|
|
233
|
+
framesLeft -= 1;
|
|
234
|
+
if (userTookOver || framesLeft <= 0) return;
|
|
235
|
+
if (!seeded) {
|
|
236
|
+
// ONE virtuoso-level jump renders the tail items into the window.
|
|
237
|
+
// Never re-issue it per frame: repeated scrollToIndex on a list whose
|
|
238
|
+
// bubble heights are still being measured keeps virtuoso in its
|
|
239
|
+
// initial-location seek (zero rows, visibility:hidden) — the exact
|
|
240
|
+
// blank-transcript deadlock initialTopMostItemIndex causes.
|
|
241
|
+
seeded = true;
|
|
242
|
+
virtuosoRef.current?.scrollToIndex({
|
|
243
|
+
index: 'LAST',
|
|
244
|
+
align: 'end',
|
|
245
|
+
offset: BOTTOM_GAP_PX,
|
|
246
|
+
behavior: 'auto',
|
|
247
|
+
});
|
|
248
|
+
raf = requestAnimationFrame(land);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
// Correction frames: as measurements land and the total height grows,
|
|
252
|
+
// keep pinning the bottom with DIRECT scrollTop writes (an ordinary
|
|
253
|
+
// scroll to virtuoso, no seek mode). A zero gap does NOT end the phase —
|
|
254
|
+
// it is usually the not-yet-measured state, and the growth we exist to
|
|
255
|
+
// chase arrives after it.
|
|
256
|
+
const el = scrollerRef.current;
|
|
257
|
+
if (el != null && el !== window && el instanceof HTMLElement) {
|
|
258
|
+
const gap = el.scrollHeight - el.clientHeight - el.scrollTop;
|
|
259
|
+
if (gap > 4) {
|
|
260
|
+
el.scrollTop = el.scrollHeight - el.clientHeight;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
raf = requestAnimationFrame(land);
|
|
264
|
+
};
|
|
265
|
+
raf = requestAnimationFrame(land);
|
|
266
|
+
return () => {
|
|
267
|
+
cancelAnimationFrame(raf);
|
|
268
|
+
scrollerEl?.removeEventListener('wheel', cancelOnUserScroll);
|
|
269
|
+
scrollerEl?.removeEventListener('touchstart', cancelOnUserScroll);
|
|
270
|
+
scrollerEl?.removeEventListener('keydown', cancelOnUserScroll);
|
|
271
|
+
};
|
|
211
272
|
}, [messages.length]);
|
|
212
273
|
|
|
213
274
|
// Force-scroll to bottom whenever the consumer bumps `scrollAnchorId`
|
|
@@ -400,16 +461,26 @@ export const MessageList = forwardRef<MessageListHandle, MessageListProps>(funct
|
|
|
400
461
|
// than the viewport (centers items near the bottom). Combined
|
|
401
462
|
// with dynamic bubble heights it triggered the same measure loop:
|
|
402
463
|
// virtuoso recomputes top padding on every size change to keep
|
|
403
|
-
// the cluster bottom-aligned.
|
|
404
|
-
//
|
|
405
|
-
//
|
|
464
|
+
// the cluster bottom-aligned. The mount-time imperative scroll
|
|
465
|
+
// already lands us at the bottom on open, which is what users
|
|
466
|
+
// actually want.
|
|
467
|
+
//
|
|
468
|
+
// No `initialTopMostItemIndex` — when the list mounts WITH
|
|
469
|
+
// history already present (a resumed engine / remounted chat),
|
|
470
|
+
// virtuoso enters its initial-location seek: it renders ZERO rows
|
|
471
|
+
// and keeps the item list `visibility: hidden` until the seek
|
|
472
|
+
// settles — and with dynamic unmeasured bubble heights that seek
|
|
473
|
+
// can never settle, leaving the whole transcript permanently
|
|
474
|
+
// blank (0 items, no error). The mount-time imperative
|
|
475
|
+
// `scrollToIndex('LAST')` effect below covers the land-at-bottom
|
|
476
|
+
// job for both mount paths (empty-then-fill AND mount-with-data),
|
|
477
|
+
// at worst costing one frame of top-anchored paint.
|
|
406
478
|
//
|
|
407
479
|
// No `increaseViewportBy` overscan — virtuoso's default (~0px)
|
|
408
480
|
// is the right call for chat: every overscanned bubble re-renders
|
|
409
481
|
// on every streaming token delta, so a 400px buffer means 3–4
|
|
410
482
|
// extra bubbles re-rendering at 60Hz during a stream. Default
|
|
411
483
|
// keeps the working set tight.
|
|
412
|
-
initialTopMostItemIndex={messages.length > 0 ? messages.length - 1 : 0}
|
|
413
484
|
atBottomThreshold={atBottomThreshold}
|
|
414
485
|
followOutput={(isAtBottom) => (isAtBottom ? 'auto' : false)}
|
|
415
486
|
scrollerRef={(el) => {
|