@schlenkr/quassel 0.5.2 → 0.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/chat-scroll.d.ts CHANGED
@@ -3,7 +3,8 @@ interface ScrollViewport {
3
3
  readonly scrollHeight: number;
4
4
  readonly clientHeight: number;
5
5
  }
6
- export declare function createChatScroll(onFollowChange?: (following: boolean) => void): {
6
+ export declare const defaultJumpToEndThreshold = 120;
7
+ export declare function createChatScroll(onFollowChange?: (following: boolean) => void, getThreshold?: () => number): {
7
8
  layout: (viewport: ScrollViewport, dragging?: boolean) => void;
8
9
  pause(viewport: ScrollViewport): void;
9
10
  scroll(viewport: ScrollViewport, dragging?: boolean): void;
package/chat.css CHANGED
@@ -771,9 +771,9 @@
771
771
  --tw-leading: 1.625;
772
772
  line-height: 1.625;
773
773
  }
774
- .qsl\:leading-\[var\(--qsl-chat-line-height\,1\.5\)\] {
775
- --tw-leading: var(--qsl-chat-line-height,1.5);
776
- line-height: var(--qsl-chat-line-height,1.5);
774
+ .qsl\:leading-\[var\(--qsl-chat-line-height\,1\.65\)\] {
775
+ --tw-leading: var(--qsl-chat-line-height,1.65);
776
+ line-height: var(--qsl-chat-line-height,1.65);
777
777
  }
778
778
  .qsl\:leading-normal {
779
779
  --tw-leading: var(--qsl-leading-normal);
@@ -1608,11 +1608,14 @@
1608
1608
  opacity: 100%;
1609
1609
  }
1610
1610
  }
1611
+ li + .qsl\:\[li\+\&\]\:mt-1 {
1612
+ margin-top: var(--qsl-spacing);
1613
+ }
1611
1614
  li > .qsl\:\[li\>\&\]\:my-0 {
1612
1615
  margin-block: 0px;
1613
1616
  }
1614
- p + .qsl\:\[p\+\&\]\:mt-2 {
1615
- margin-top: calc(var(--qsl-spacing) * 2);
1617
+ p + .qsl\:\[p\+\&\]\:mt-3 {
1618
+ margin-top: calc(var(--qsl-spacing) * 3);
1616
1619
  }
1617
1620
  pre > .qsl\:\[pre\>\&\]\:bg-transparent {
1618
1621
  background-color: transparent;
package/index.js CHANGED
@@ -170,9 +170,10 @@ import { createPortal } from "react-dom";
170
170
  import { Dialog } from "@base-ui/react/dialog";
171
171
 
172
172
  // packages/quassel/src/chat-scroll.ts
173
- var atEnd = (viewport) => viewport.scrollHeight - viewport.clientHeight - viewport.scrollTop <= 24;
173
+ var defaultJumpToEndThreshold = 120;
174
174
  function createChatScroll(onFollowChange = () => {
175
- }) {
175
+ }, getThreshold = () => defaultJumpToEndThreshold) {
176
+ const atEnd = (viewport) => viewport.scrollHeight - viewport.clientHeight - viewport.scrollTop <= getThreshold();
176
177
  let following = true;
177
178
  let lastTop = 0;
178
179
  let lastHeight = 0;
@@ -191,18 +192,18 @@ function createChatScroll(onFollowChange = () => {
191
192
  const layout = (viewport, dragging = false) => {
192
193
  if (viewport.clientHeight <= 0) return;
193
194
  if (movedUp(viewport, dragging) && !atEnd(viewport)) follow(false);
195
+ if (atEnd(viewport)) follow(true);
194
196
  if (following) viewport.scrollTop = Math.max(0, viewport.scrollHeight - viewport.clientHeight);
195
- else if (viewport.scrollHeight <= viewport.clientHeight) follow(true);
196
197
  remember(viewport);
197
198
  };
198
199
  return {
199
200
  layout,
200
201
  pause(viewport) {
201
- if (viewport.scrollHeight > viewport.clientHeight) follow(false);
202
+ if (viewport.clientHeight > 0) follow(atEnd(viewport));
202
203
  },
203
204
  scroll(viewport, dragging = false) {
204
205
  if (viewport.clientHeight <= 0) return;
205
- if (atEnd(viewport) && viewport.scrollTop > lastTop) follow(true);
206
+ if (atEnd(viewport)) follow(true);
206
207
  else if (movedUp(viewport, dragging) && !atEnd(viewport)) follow(false);
207
208
  remember(viewport);
208
209
  },
@@ -535,10 +536,10 @@ var components = {
535
536
  h4: ({ children }) => /* @__PURE__ */ jsx8("h4", { className: "qsl:mt-2 qsl:mb-1 qsl:font-medium", children }),
536
537
  h5: ({ children }) => /* @__PURE__ */ jsx8("h5", { className: "qsl:mt-2 qsl:mb-1 qsl:font-medium", children }),
537
538
  h6: ({ children }) => /* @__PURE__ */ jsx8("h6", { className: "qsl:mt-2 qsl:mb-1 qsl:font-medium", children }),
538
- p: ({ children }) => /* @__PURE__ */ jsx8("p", { className: "qsl:[p+&]:mt-2", children }),
539
- ul: ({ children }) => /* @__PURE__ */ jsx8("ul", { className: "qsl:my-2 qsl:list-disc qsl:pl-6 qsl:[li>&]:my-0", children }),
540
- ol: ({ children }) => /* @__PURE__ */ jsx8("ol", { className: "qsl:my-2 qsl:list-decimal qsl:pl-6 qsl:[li>&]:my-0", children }),
541
- li: ({ children }) => /* @__PURE__ */ jsx8("li", { className: "qsl:marker:text-muted-foreground qsl:in-data-[tone=on-color]:marker:text-inherit", children }),
539
+ p: ({ children }) => /* @__PURE__ */ jsx8("p", { className: "qsl:[p+&]:mt-3", children }),
540
+ ul: ({ children }) => /* @__PURE__ */ jsx8("ul", { className: "qsl:my-3 qsl:list-disc qsl:pl-6 qsl:[li>&]:my-0", children }),
541
+ ol: ({ children }) => /* @__PURE__ */ jsx8("ol", { className: "qsl:my-3 qsl:list-decimal qsl:pl-6 qsl:[li>&]:my-0", children }),
542
+ li: ({ children }) => /* @__PURE__ */ jsx8("li", { className: "qsl:[li+&]:mt-1 qsl:marker:text-muted-foreground qsl:in-data-[tone=on-color]:marker:text-inherit", children }),
542
543
  hr: () => /* @__PURE__ */ jsx8("hr", { className: "qsl:my-3 qsl:border-t qsl:border-border" }),
543
544
  img: ({ src, alt }) => /* @__PURE__ */ jsx8("img", { alt, className: "qsl:h-auto qsl:max-w-full", src: typeof src === "string" ? src : void 0 }),
544
545
  blockquote: ({ children }) => /* @__PURE__ */ jsx8("blockquote", { className: "qsl:my-1 qsl:border-l-2 qsl:border-border qsl:pl-3 qsl:text-muted-foreground qsl:in-data-[tone=on-color]:border-white/50 qsl:in-data-[tone=on-color]:text-inherit", children }),
@@ -1190,7 +1191,7 @@ var chipClasses = cn(
1190
1191
  var chipInteractiveClasses = "qsl:cursor-pointer qsl:hover:border-border qsl:hover:bg-secondary qsl:hover:text-foreground qsl:focus-visible:border-border qsl:focus-visible:bg-secondary qsl:focus-visible:text-foreground qsl:focus-visible:outline-none qsl:data-[state=done]:hover:opacity-100 qsl:data-[state=done]:focus-visible:opacity-100";
1191
1192
  var collapseAtBottomFrom = 11;
1192
1193
  var maxAnnouncementsAtOnce = 2;
1193
- var bubbleBodyClasses = "qsl:relative qsl:max-w-[86%] qsl:px-3 qsl:py-2 qsl:leading-[var(--qsl-chat-line-height,1.5)] qsl:[overflow-wrap:anywhere] qsl:@max-[620px]/chat:max-w-full";
1194
+ var bubbleBodyClasses = "qsl:relative qsl:max-w-[86%] qsl:px-3 qsl:py-2 qsl:leading-[var(--qsl-chat-line-height,1.65)] qsl:[overflow-wrap:anywhere] qsl:@max-[620px]/chat:max-w-full";
1194
1195
  function isStep(message) {
1195
1196
  return !!message && (message.role === "thinking" || message.role === "tool");
1196
1197
  }
@@ -1223,7 +1224,7 @@ function ChatMessages({
1223
1224
  scrollerRef,
1224
1225
  maxWidth,
1225
1226
  horizontalPadding,
1226
- jumpToEndThreshold = 120,
1227
+ jumpToEndThreshold = defaultJumpToEndThreshold,
1227
1228
  appearance,
1228
1229
  timestampOptions,
1229
1230
  codeBlockOptions,
@@ -1253,7 +1254,11 @@ function ChatMessages({
1253
1254
  const scrollbarDragging = useRef3(false);
1254
1255
  const touchY = useRef3(void 0);
1255
1256
  const [showJumpToEnd, setShowJumpToEnd] = useState4(false);
1256
- const [scroll] = useState4(() => createChatScroll());
1257
+ const endThreshold = useRef3(jumpToEndThreshold);
1258
+ useLayoutEffect(() => {
1259
+ endThreshold.current = jumpToEndThreshold;
1260
+ }, [jumpToEndThreshold]);
1261
+ const [scroll] = useState4(() => createChatScroll((following) => setShowJumpToEnd(!following), () => endThreshold.current));
1257
1262
  const sendScope = useContext3(ChatSendContext);
1258
1263
  const dock = useContext3(ChatActionDock);
1259
1264
  const [now, setNow] = useState4(() => Date.now());
@@ -1275,15 +1280,11 @@ function ChatMessages({
1275
1280
  }
1276
1281
  due.filter((message) => selected.includes(message)).map((message) => announce2 ? announce2({ kind: message.role === "action" ? "action" : "reply", message }) : markdownPlainText(message.text)).forEach((text) => text && announce(text));
1277
1282
  }, [messages, selected, running2, announce2]);
1278
- const updateJumpVisibility = useCallback2((viewport) => {
1279
- if (viewport.clientHeight > 0) setShowJumpToEnd(viewport.scrollHeight - viewport.clientHeight - viewport.scrollTop > jumpToEndThreshold);
1280
- }, [jumpToEndThreshold]);
1281
1283
  useLayoutEffect(() => sendScope?.subscribe(() => {
1282
1284
  const viewport = scrollArea.current;
1283
1285
  if (!viewport) return;
1284
1286
  scroll.jump(viewport);
1285
- updateJumpVisibility(viewport);
1286
- }), [sendScope, scroll, updateJumpVisibility]);
1287
+ }), [sendScope, scroll]);
1287
1288
  const pauseUp = (viewport, target) => {
1288
1289
  let element = target instanceof Element ? target : null;
1289
1290
  while (element && element !== viewport) {
@@ -1296,9 +1297,8 @@ function ChatMessages({
1296
1297
  const area = scrollArea.current;
1297
1298
  if (area) {
1298
1299
  scroll.scroll(area, scrollbarDragging.current);
1299
- updateJumpVisibility(area);
1300
1300
  }
1301
- }, [scroll, updateJumpVisibility]);
1301
+ }, [scroll]);
1302
1302
  useEffect3(() => {
1303
1303
  const finishDrag = () => {
1304
1304
  if (scrollbarDragging.current && scrollArea.current) scroll.scroll(scrollArea.current, true);
@@ -1316,7 +1316,6 @@ function ChatMessages({
1316
1316
  if (!area) return;
1317
1317
  const align = () => {
1318
1318
  scroll.layout(area, scrollbarDragging.current);
1319
- updateJumpVisibility(area);
1320
1319
  };
1321
1320
  align();
1322
1321
  const observer = new ResizeObserver(align);
@@ -1520,7 +1519,6 @@ function ChatMessages({
1520
1519
  showJumpToEnd && /* @__PURE__ */ jsx13("div", { className: "qsl:pointer-events-none qsl:absolute qsl:inset-x-0 qsl:bottom-[calc(var(--qsl-composer-height,0px)_+_12px)] qsl:z-10 qsl:flex qsl:justify-center", children: /* @__PURE__ */ jsx13(Button2, { "aria-label": allTexts.jumpToEnd, className: "qsl:pointer-events-auto qsl:rounded-full qsl:border qsl:border-primary-foreground/20 qsl:shadow-md", size: "icon", title: allTexts.jumpToEnd, variant: "default", onClick: () => {
1521
1520
  if (scrollArea.current) {
1522
1521
  scroll.jump(scrollArea.current);
1523
- updateJumpVisibility(scrollArea.current);
1524
1522
  }
1525
1523
  }, children: /* @__PURE__ */ jsx13(ChevronDownIcon, {}) }) }),
1526
1524
  dock && openActions.length > 0 && createPortal(
@@ -1785,7 +1783,7 @@ function Bubble({
1785
1783
  /* @__PURE__ */ jsx13(MessageActions, { message, options: messageActions, texts })
1786
1784
  ] });
1787
1785
  const cursor = message.role === "assistant" && message.textCursor ? JSON.stringify(message.textCursor) : void 0;
1788
- if (!useBubble) return /* @__PURE__ */ jsx13("div", { "data-chat-text-cursor": cursor, className: cn(stepSpacing, "qsl:group qsl:relative qsl:w-full qsl:leading-[var(--qsl-chat-line-height,1.5)] qsl:text-foreground"), "data-message": "answer", children: content });
1786
+ if (!useBubble) return /* @__PURE__ */ jsx13("div", { "data-chat-text-cursor": cursor, className: cn(stepSpacing, "qsl:group qsl:relative qsl:w-full qsl:leading-[var(--qsl-chat-line-height,1.65)] qsl:text-foreground"), "data-message": "answer", children: content });
1789
1787
  const fullBackground = message.role === "user" && bubbleOptions?.userBackgroundFullWidth !== false;
1790
1788
  const surfaceClasses = message.bubble ? "qsl:rounded-lg" : "qsl:rounded-[var(--qsl-bubble-radius,10px_10px_4px_10px)] qsl:border qsl:border-border qsl:bg-secondary qsl:in-data-[tone=material]:rounded-[var(--qsl-bubble-radius,9px)] qsl:in-data-[tone=material]:border-glass-edge/20 qsl:in-data-[tone=material]:bg-primary/15";
1791
1789
  return /* @__PURE__ */ jsxs6(
@@ -2170,7 +2168,7 @@ function ChatInputToolbar({
2170
2168
  fileInput.dragging && "qsl:outline-2 qsl:outline-offset-[3px] qsl:outline-dashed qsl:outline-primary"
2171
2169
  );
2172
2170
  const textareaClasses = cn(
2173
- "qsl:resize-none qsl:overscroll-contain qsl:border-0 qsl:bg-transparent qsl:text-chat qsl:leading-[var(--qsl-chat-line-height,1.5)] qsl:text-foreground qsl:placeholder:text-muted-foreground qsl:focus:outline-none",
2171
+ "qsl:resize-none qsl:overscroll-contain qsl:border-0 qsl:bg-transparent qsl:text-chat qsl:leading-[var(--qsl-chat-line-height,1.65)] qsl:text-foreground qsl:placeholder:text-muted-foreground qsl:focus:outline-none",
2174
2172
  inline ? "qsl:min-w-0 qsl:flex-1 qsl:px-2.5 qsl:py-2" : toolbar ? "qsl:w-full qsl:min-w-0 qsl:flex-auto qsl:px-2 qsl:py-[3px] qsl:leading-[1.3]" : "qsl:px-3 qsl:pt-3 qsl:pb-2"
2175
2173
  );
2176
2174
  const toolbarRowClasses = cn(