quassel 0.2.0 → 0.2.2

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/ChatMessages.d.ts CHANGED
@@ -7,7 +7,7 @@ import { type ChatAppearance, type TimestampOptions, type CodeBlockOptions, type
7
7
  * Der Verlauf: eigener Scroll-Container mit Autoscroll, solange man unten ist,
8
8
  * und einem Zum-Ende-Knopf, sobald man liest. Ohne Eingabe read-only nutzbar.
9
9
  */
10
- export declare function ChatMessages({ messages, owner, detailMode, running, working, stepsExpandable, texts, toolArgumentsText, renderTool, renderAction, onDismissAction, onLinkClick, emptyState, className, showTimestamps, announce, scrollerRef, maxWidth, horizontalPadding, jumpToEndThreshold, appearance, timestampOptions, codeBlockOptions, bubbleOptions, messageActions, }: {
10
+ export declare function ChatMessages({ messages, owner, detailMode, running, working, stepsExpandable, texts, toolArgumentsText, renderTool, renderAction, onDismissAction, onLinkClick, emptyState, className, scrollerClassName, showTimestamps, announce, scrollerRef, maxWidth, horizontalPadding, jumpToEndThreshold, appearance, timestampOptions, codeBlockOptions, bubbleOptions, messageActions, }: {
11
11
  messages: Message[];
12
12
  /** Beiträge mit diesem Message.sender erscheinen ohne Sprechblase; null behält die Nachrichtenvorgaben. */
13
13
  owner?: string | null;
@@ -27,6 +27,8 @@ export declare function ChatMessages({ messages, owner, detailMode, running, wor
27
27
  onLinkClick?: LinkClickHandler;
28
28
  emptyState?: ReactNode;
29
29
  className?: string;
30
+ /** Klassen des Hosts für den Scroll-Container (data-quassel-transcript). */
31
+ scrollerClassName?: string;
30
32
  /** true = dezente HH:MM-Spalte links an jedem Block (Message.at). */
31
33
  showTimestamps?: boolean;
32
34
  /** Screenreader-Ansage fertiger Antworten und neuer Aktionen; false = der Host sagt selbst an. */
@@ -1,4 +1,4 @@
1
- import { type ComponentType, type ReactNode } from "react";
1
+ import { type ComponentType, type ReactNode, type RefObject } from "react";
2
2
  export type QuasselButtonVariant = "default" | "outline" | "secondary" | "ghost" | "destructive" | "link";
3
3
  export type QuasselButtonSize = "default" | "xs" | "sm" | "lg" | "icon" | "icon-xs" | "icon-sm" | "icon-lg";
4
4
  export interface QuasselButtonProps {
@@ -45,6 +45,9 @@ export interface QuasselPopoverAnchor {
45
45
  getBoundingClientRect: () => DOMRect;
46
46
  contextElement?: Element;
47
47
  }
48
+ /** Where popovers mount: an element, a ref or a getter; resolved when the popover opens. */
49
+ export type QuasselPortalTarget = HTMLElement | ShadowRoot;
50
+ export type QuasselPortalContainer = QuasselPortalTarget | RefObject<QuasselPortalTarget | null> | (() => QuasselPortalTarget | null | undefined);
48
51
  export interface QuasselPopoverContentProps {
49
52
  anchor?: QuasselPopoverAnchor;
50
53
  align?: "start" | "center" | "end";
@@ -53,6 +56,8 @@ export interface QuasselPopoverContentProps {
53
56
  collisionPadding?: number;
54
57
  className?: string;
55
58
  "aria-label"?: string;
59
+ /** The resolved portalContainer of QuasselProvider; undefined = document.body. */
60
+ portalContainer?: QuasselPortalTarget;
56
61
  children?: ReactNode;
57
62
  }
58
63
  /** The primitives quassel renders; a host replaces any of them through QuasselProvider. */
@@ -65,12 +70,15 @@ export interface QuasselComponents {
65
70
  PopoverContent: ComponentType<QuasselPopoverContentProps>;
66
71
  }
67
72
  export declare const defaultComponents: QuasselComponents;
68
- export declare function QuasselProvider({ components, allowUrl, children }: {
73
+ export declare function QuasselProvider({ components, allowUrl, portalContainer, children }: {
69
74
  /** Replaces single primitives; the rest stay the built-in ones or those of an outer provider. */
70
75
  components?: Partial<QuasselComponents>;
71
76
  /** Keeps Markdown link and image URLs that the default policy (http, https, mailto, tel, ftp, irc, xmpp, relative) would drop. */
72
77
  allowUrl?: (url: string) => boolean;
78
+ /** Mounts quassel's popovers inside this element instead of document.body, e.g. within the host's theme wrapper. */
79
+ portalContainer?: QuasselPortalContainer;
73
80
  children: ReactNode;
74
81
  }): import("react").JSX.Element;
75
82
  export declare const useQuasselComponents: () => QuasselComponents;
76
83
  export declare const useAllowUrl: () => ((url: string) => boolean) | undefined;
84
+ export declare function useQuasselPortalContainer(): QuasselPortalTarget | undefined;
package/README.md CHANGED
@@ -4,7 +4,7 @@ Chat building blocks for React: a streaming transcript with thinking and tool st
4
4
  actions, an input card with attachments and steering, and a small event contract that works
5
5
  on the server as well. quassel is the chat of [RAgents](https://github.com/SchlenkR/RAgents),
6
6
  published as a library. It ships a compiled stylesheet, so hosts need no Tailwind, and every
7
- color, radius and font is a `--qsl-*` variable.
7
+ color, radius, font and font size is a `--qsl-*` variable.
8
8
 
9
9
  ![An agent chat with grouped steps, timestamps, Markdown and the input card](https://raw.githubusercontent.com/SchlenkR/quassel/main/docs/images/agent.png)
10
10
 
@@ -27,7 +27,9 @@ rendering per action owner. Resolved actions stay in the transcript as a record.
27
27
  </p>
28
28
 
29
29
  **Your look, not a fork.** Override `--qsl-*` variables on any wrapper, switch light and dark
30
- with `data-theme`, or hand quassel your own Button, Toggle, Card and Popover components.
30
+ with `data-theme`, or hand quassel your own Button, Toggle, Card and Popover components. With
31
+ `portalContainer` on `QuasselProvider`, popovers render inside your wrapper and pick up its
32
+ variables too.
31
33
 
32
34
  ![Default, sunset and emerald themes](https://raw.githubusercontent.com/SchlenkR/quassel/main/docs/images/themes.png)
33
35
 
package/chat.css CHANGED
@@ -18,11 +18,12 @@
18
18
  --qsl-font-weight-semibold: 600;
19
19
  --qsl-leading-normal: 1.5;
20
20
  --qsl-leading-relaxed: 1.625;
21
- --qsl-radius-md: calc(var(--qsl-radius) * 0.8);
22
- --qsl-radius-xl: calc(var(--qsl-radius) * 1.4);
23
21
  --qsl-animate-spin: qsl-spin 1s linear infinite;
24
22
  --qsl-default-transition-duration: 150ms;
25
23
  --qsl-default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
24
+ --qsl-text-meta: 10.5px;
25
+ --qsl-text-trace: 11px;
26
+ --qsl-text-label: 0.72rem;
26
27
  --qsl-radius-panel: 17px;
27
28
  --qsl-animate-fade-pulse: qsl-fade-pulse 1.9s cubic-bezier(0.2, 0.6, 0.35, 1) infinite;
28
29
  --qsl-animate-working-pulse: qsl-working-pulse 1.6s ease-in-out infinite;
@@ -405,32 +406,32 @@
405
406
  .qsl\:overflow-y-auto {
406
407
  overflow-y: auto;
407
408
  }
408
- .qsl\:rounded-\[10px_10px_4px_10px\] {
409
- border-radius: 10px 10px 4px 10px;
409
+ .qsl\:rounded-\[min\(--theme\(--radius-md\)\,10px\)\] {
410
+ border-radius: min(var(--qsl-radius-md, calc(var(--qsl-radius) * 0.8)), 10px);
410
411
  }
411
- .qsl\:rounded-\[min\(var\(--qsl-radius-md\)\,10px\)\] {
412
- border-radius: min(var(--qsl-radius-md), 10px);
412
+ .qsl\:rounded-\[min\(--theme\(--radius-md\)\,12px\)\] {
413
+ border-radius: min(var(--qsl-radius-md, calc(var(--qsl-radius) * 0.8)), 12px);
413
414
  }
414
- .qsl\:rounded-\[min\(var\(--qsl-radius-md\)\,12px\)\] {
415
- border-radius: min(var(--qsl-radius-md), 12px);
415
+ .qsl\:rounded-\[var\(--qsl-bubble-radius\,10px_10px_4px_10px\)\] {
416
+ border-radius: var(--qsl-bubble-radius,10px 10px 4px 10px);
416
417
  }
417
- .qsl\:rounded-\[var\(--qsl-input-card-radius\,var\(--qsl-radius-xl\)\)\] {
418
- border-radius: var(--qsl-input-card-radius,var(--qsl-radius-xl));
418
+ .qsl\:rounded-\[var\(--qsl-input-card-radius\,--theme\(--radius-xl\)\)\] {
419
+ border-radius: var(--qsl-input-card-radius,var(--qsl-radius-xl, calc(var(--qsl-radius) * 1.4)));
419
420
  }
420
421
  .qsl\:rounded-full {
421
422
  border-radius: calc(infinity * 1px);
422
423
  }
423
424
  .qsl\:rounded-lg {
424
- border-radius: var(--qsl-radius);
425
+ border-radius: var(--qsl-radius-lg, var(--qsl-radius));
425
426
  }
426
427
  .qsl\:rounded-md {
427
- border-radius: calc(var(--qsl-radius) * 0.8);
428
+ border-radius: var(--qsl-radius-md, calc(var(--qsl-radius) * 0.8));
428
429
  }
429
430
  .qsl\:rounded-panel {
430
431
  border-radius: var(--qsl-radius-panel);
431
432
  }
432
433
  .qsl\:rounded-sm {
433
- border-radius: calc(var(--qsl-radius) * 0.6);
434
+ border-radius: var(--qsl-radius-sm, calc(var(--qsl-radius) * 0.6));
434
435
  }
435
436
  .qsl\:border {
436
437
  border-style: var(--tw-border-style);
@@ -667,15 +668,6 @@
667
668
  .qsl\:text-\[0\.8rem\] {
668
669
  font-size: 0.8rem;
669
670
  }
670
- .qsl\:text-\[0\.72rem\] {
671
- font-size: 0.72rem;
672
- }
673
- .qsl\:text-\[10\.5px\] {
674
- font-size: 10.5px;
675
- }
676
- .qsl\:text-\[11px\] {
677
- font-size: 11px;
678
- }
679
671
  .qsl\:text-\[13px\] {
680
672
  font-size: 13px;
681
673
  }
@@ -685,6 +677,15 @@
685
677
  .qsl\:text-\[length\:var\(--qsl-chat-font-size\,13px\)\] {
686
678
  font-size: var(--qsl-chat-font-size,13px);
687
679
  }
680
+ .qsl\:text-label {
681
+ font-size: var(--qsl-text-label);
682
+ }
683
+ .qsl\:text-meta {
684
+ font-size: var(--qsl-text-meta);
685
+ }
686
+ .qsl\:text-trace {
687
+ font-size: var(--qsl-text-trace);
688
+ }
688
689
  .qsl\:leading-\[1\.3\] {
689
690
  --tw-leading: 1.3;
690
691
  line-height: 1.3;
@@ -1071,13 +1072,13 @@
1071
1072
  display: none;
1072
1073
  }
1073
1074
  :where([data-slot="button-group"]) .qsl\:in-data-\[slot\=button-group\]\:rounded-lg {
1074
- border-radius: var(--qsl-radius);
1075
+ border-radius: var(--qsl-radius-lg, var(--qsl-radius));
1075
1076
  }
1076
- :where([data-tone="material"]) .qsl\:in-data-\[tone\=material\]\:rounded-\[9px\] {
1077
- border-radius: 9px;
1077
+ :where([data-tone="material"]) .qsl\:in-data-\[tone\=material\]\:rounded-\[var\(--qsl-bubble-radius\,9px\)\] {
1078
+ border-radius: var(--qsl-bubble-radius,9px);
1078
1079
  }
1079
1080
  :where([data-tone="material"]) .qsl\:in-data-\[tone\=material\]\:rounded-xl {
1080
- border-radius: calc(var(--qsl-radius) * 1.4);
1081
+ border-radius: var(--qsl-radius-xl, calc(var(--qsl-radius) * 1.4));
1081
1082
  }
1082
1083
  :where([data-tone="material"]) .qsl\:in-data-\[tone\=material\]\:border-glass-edge\/20 {
1083
1084
  border-color: var(--qsl-glass-edge);
@@ -1512,12 +1513,12 @@
1512
1513
  background-color: var(--qsl-border);
1513
1514
  }
1514
1515
  :is(.qsl\:\*\:\[img\:first-child\]\:rounded-t-xl > *):is(img:first-child) {
1515
- border-top-left-radius: calc(var(--qsl-radius) * 1.4);
1516
- border-top-right-radius: calc(var(--qsl-radius) * 1.4);
1516
+ border-top-left-radius: var(--qsl-radius-xl, calc(var(--qsl-radius) * 1.4));
1517
+ border-top-right-radius: var(--qsl-radius-xl, calc(var(--qsl-radius) * 1.4));
1517
1518
  }
1518
1519
  :is(.qsl\:\*\:\[img\:last-child\]\:rounded-b-xl > *):is(img:last-child) {
1519
- border-bottom-right-radius: calc(var(--qsl-radius) * 1.4);
1520
- border-bottom-left-radius: calc(var(--qsl-radius) * 1.4);
1520
+ border-bottom-right-radius: var(--qsl-radius-xl, calc(var(--qsl-radius) * 1.4));
1521
+ border-bottom-left-radius: var(--qsl-radius-xl, calc(var(--qsl-radius) * 1.4));
1521
1522
  }
1522
1523
  .qsl\:\[\&\>\*\:first-child\]\:mt-0 > :first-child {
1523
1524
  margin-top: 0px;
@@ -1656,10 +1657,6 @@
1656
1657
  --qsl-pop-shadow: 0 12px 36px #49384e26;
1657
1658
  --qsl-card-shadow: 0 12px 22px #49384e2c;
1658
1659
  }
1659
- :where([data-quassel]) {
1660
- --qsl-radius-md: calc(var(--qsl-radius) * 0.8);
1661
- --qsl-radius-xl: calc(var(--qsl-radius) * 1.4);
1662
- }
1663
1660
  :where([data-theme="dark"]) {
1664
1661
  --qsl-color-scheme: dark;
1665
1662
  --qsl-background: #292735;
@@ -1750,7 +1747,7 @@
1750
1747
  font-size: 1em;
1751
1748
  }
1752
1749
  :where([data-quassel], [data-quassel] *):where(small) {
1753
- font-size: 80%;
1750
+ font-size: var(--qsl-text-small, 80%);
1754
1751
  }
1755
1752
  :where([data-quassel], [data-quassel] *):where(sub, sup) {
1756
1753
  font-size: 75%;
package/index.d.ts CHANGED
@@ -20,4 +20,4 @@ export { messageDate, timestampDay, timestampLabel } from "./timestamps";
20
20
  export { attachmentCapabilityError, attachmentDownloadUrl, attachmentMediaType, encodeAttachment, formatAttachmentSize, validateAttachmentSelection, MAX_CHAT_ATTACHMENTS, MAX_CHAT_ATTACHMENT_BYTES, } from "./attachments";
21
21
  export type { ChatAppearance, TimestampOptions, CodeBlockOptions, BubbleOptions, MessageAction, MessageActionsOptions, SendShortcut, } from "./options";
22
22
  export { appearanceStyle } from "./options";
23
- export { QuasselProvider, defaultComponents, useQuasselComponents, type QuasselComponents, type QuasselButtonProps, type QuasselButtonVariant, type QuasselButtonSize, type QuasselToggleProps, type QuasselCardProps, type QuasselStopButtonProps, type QuasselPopoverProps, type QuasselPopoverContentProps, type QuasselPopoverAnchor, } from "./QuasselProvider";
23
+ export { QuasselProvider, defaultComponents, useQuasselComponents, useQuasselPortalContainer, type QuasselComponents, type QuasselButtonProps, type QuasselButtonVariant, type QuasselButtonSize, type QuasselToggleProps, type QuasselCardProps, type QuasselStopButtonProps, type QuasselPopoverProps, type QuasselPopoverContentProps, type QuasselPopoverAnchor, type QuasselPortalContainer, type QuasselPortalTarget, } from "./QuasselProvider";
package/index.js CHANGED
@@ -42,7 +42,6 @@ var defaultTexts = {
42
42
  placeholder: "Nachricht schreiben ...",
43
43
  steeringPlaceholder: "Dazwischenfunken ...",
44
44
  steered: "In den laufenden Turn eingespeist",
45
- inputHint: "Enter zum Senden - Shift + Enter f\xFCr eine neue Zeile",
46
45
  detailModeTitle: "Detailgrad der Schritte",
47
46
  detailModeOff: "nur Antworten",
48
47
  detailModeCurrent: "aktuell",
@@ -87,7 +86,7 @@ var defaultTexts = {
87
86
 
88
87
  // packages/quassel/src/ui/cn.ts
89
88
  import { createCn } from "cn/config";
90
- var cn = createCn({ prefix: "qsl" });
89
+ var cn = createCn({ prefix: "qsl", extend: { theme: { text: ["meta", "trace", "label"] } } });
91
90
 
92
91
  // packages/quassel/src/ChatMessages.tsx
93
92
  import { useCallback as useCallback2, useContext as useContext3, useEffect as useEffect3, useLayoutEffect, useMemo as useMemo3, useRef as useRef3, useState as useState4 } from "react";
@@ -154,12 +153,12 @@ var buttonVariants = cva(
154
153
  },
155
154
  size: {
156
155
  default: "qsl:h-8 qsl:gap-1.5 qsl:px-2.5 qsl:has-data-[icon=inline-end]:pr-2 qsl:has-data-[icon=inline-start]:pl-2",
157
- xs: "qsl:h-6 qsl:gap-1 qsl:rounded-[min(var(--qsl-radius-md),10px)] qsl:px-2 qsl:text-xs qsl:in-data-[slot=button-group]:rounded-lg qsl:has-data-[icon=inline-end]:pr-1.5 qsl:has-data-[icon=inline-start]:pl-1.5 qsl:[&_svg:not([class*='size-'])]:size-3",
158
- sm: "qsl:h-7 qsl:gap-1 qsl:rounded-[min(var(--qsl-radius-md),12px)] qsl:px-2.5 qsl:text-[0.8rem] qsl:in-data-[slot=button-group]:rounded-lg qsl:has-data-[icon=inline-end]:pr-1.5 qsl:has-data-[icon=inline-start]:pl-1.5 qsl:[&_svg:not([class*='size-'])]:size-3.5",
156
+ xs: "qsl:h-6 qsl:gap-1 qsl:rounded-[min(--theme(--radius-md),10px)] qsl:px-2 qsl:text-xs qsl:in-data-[slot=button-group]:rounded-lg qsl:has-data-[icon=inline-end]:pr-1.5 qsl:has-data-[icon=inline-start]:pl-1.5 qsl:[&_svg:not([class*='size-'])]:size-3",
157
+ sm: "qsl:h-7 qsl:gap-1 qsl:rounded-[min(--theme(--radius-md),12px)] qsl:px-2.5 qsl:text-[0.8rem] qsl:in-data-[slot=button-group]:rounded-lg qsl:has-data-[icon=inline-end]:pr-1.5 qsl:has-data-[icon=inline-start]:pl-1.5 qsl:[&_svg:not([class*='size-'])]:size-3.5",
159
158
  lg: "qsl:h-9 qsl:gap-1.5 qsl:px-2.5 qsl:has-data-[icon=inline-end]:pr-2 qsl:has-data-[icon=inline-start]:pl-2",
160
159
  icon: "qsl:size-8",
161
- "icon-xs": "qsl:size-6 qsl:rounded-[min(var(--qsl-radius-md),10px)] qsl:in-data-[slot=button-group]:rounded-lg qsl:[&_svg:not([class*='size-'])]:size-3",
162
- "icon-sm": "qsl:size-7 qsl:rounded-[min(var(--qsl-radius-md),12px)] qsl:in-data-[slot=button-group]:rounded-lg",
160
+ "icon-xs": "qsl:size-6 qsl:rounded-[min(--theme(--radius-md),10px)] qsl:in-data-[slot=button-group]:rounded-lg qsl:[&_svg:not([class*='size-'])]:size-3",
161
+ "icon-sm": "qsl:size-7 qsl:rounded-[min(--theme(--radius-md),12px)] qsl:in-data-[slot=button-group]:rounded-lg",
163
162
  "icon-lg": "qsl:size-9"
164
163
  }
165
164
  },
@@ -223,9 +222,10 @@ function PopoverContent({
223
222
  anchor,
224
223
  collisionPadding,
225
224
  keepMounted,
225
+ portalContainer,
226
226
  ...props
227
227
  }) {
228
- return /* @__PURE__ */ jsx3(PopoverPrimitive.Portal, { keepMounted, children: /* @__PURE__ */ jsx3(
228
+ return /* @__PURE__ */ jsx3(PopoverPrimitive.Portal, { container: portalContainer, keepMounted, children: /* @__PURE__ */ jsx3(
229
229
  PopoverPrimitive.Positioner,
230
230
  {
231
231
  align,
@@ -239,6 +239,7 @@ function PopoverContent({
239
239
  PopoverPrimitive.Popup,
240
240
  {
241
241
  "data-quassel": "",
242
+ "data-quassel-popover": "",
242
243
  "data-slot": "popover-content",
243
244
  className: cn(
244
245
  "qsl:z-[110] qsl:flex qsl:w-72 qsl:origin-(--transform-origin) qsl:flex-col qsl:gap-2.5 qsl:rounded-lg qsl:bg-popover qsl:p-2.5 qsl:text-sm qsl:text-popover-foreground qsl:shadow-md qsl:ring-1 qsl:ring-foreground/10 qsl:outline-hidden qsl:duration-100 qsl:data-[side=bottom]:slide-in-from-top-2 qsl:data-[side=inline-end]:slide-in-from-left-2 qsl:data-[side=inline-start]:slide-in-from-right-2 qsl:data-[side=left]:slide-in-from-right-2 qsl:data-[side=right]:slide-in-from-left-2 qsl:data-[side=top]:slide-in-from-bottom-2 qsl:data-open:animate-in qsl:data-open:fade-in-0 qsl:data-open:zoom-in-95 qsl:data-closed:animate-out qsl:data-closed:fade-out-0 qsl:data-closed:zoom-out-95",
@@ -285,7 +286,7 @@ var toggleVariants = cva2(
285
286
  },
286
287
  size: {
287
288
  default: "qsl:h-8 qsl:min-w-8 qsl:px-2.5 qsl:has-data-[icon=inline-end]:pr-2 qsl:has-data-[icon=inline-start]:pl-2",
288
- sm: "qsl:h-7 qsl:min-w-7 qsl:rounded-[min(var(--qsl-radius-md),12px)] qsl:px-2.5 qsl:text-[0.8rem] qsl:has-data-[icon=inline-end]:pr-1.5 qsl:has-data-[icon=inline-start]:pl-1.5 qsl:[&_svg:not([class*='size-'])]:size-3.5",
289
+ sm: "qsl:h-7 qsl:min-w-7 qsl:rounded-[min(--theme(--radius-md),12px)] qsl:px-2.5 qsl:text-[0.8rem] qsl:has-data-[icon=inline-end]:pr-1.5 qsl:has-data-[icon=inline-start]:pl-1.5 qsl:[&_svg:not([class*='size-'])]:size-3.5",
289
290
  lg: "qsl:h-9 qsl:min-w-9 qsl:px-2.5 qsl:has-data-[icon=inline-end]:pr-2 qsl:has-data-[icon=inline-start]:pl-2"
290
291
  }
291
292
  },
@@ -316,16 +317,22 @@ function Toggle({
316
317
  import { jsx as jsx7 } from "react/jsx-runtime";
317
318
  var defaultComponents = { Button, Toggle, Card, StopButton, Popover, PopoverContent };
318
319
  var QuasselContext = createContext({ components: defaultComponents });
319
- function QuasselProvider({ components: components2, allowUrl, children }) {
320
+ function QuasselProvider({ components: components2, allowUrl, portalContainer, children }) {
320
321
  const outer = useContext(QuasselContext);
321
322
  const value = useMemo(() => ({
322
323
  components: { ...outer.components, ...components2 },
323
- allowUrl: allowUrl ?? outer.allowUrl
324
- }), [outer, components2, allowUrl]);
324
+ allowUrl: allowUrl ?? outer.allowUrl,
325
+ portalContainer: portalContainer ?? outer.portalContainer
326
+ }), [outer, components2, allowUrl, portalContainer]);
325
327
  return /* @__PURE__ */ jsx7(QuasselContext.Provider, { value, children });
326
328
  }
327
329
  var useQuasselComponents = () => useContext(QuasselContext).components;
328
330
  var useAllowUrl = () => useContext(QuasselContext).allowUrl;
331
+ function useQuasselPortalContainer() {
332
+ const container = useContext(QuasselContext).portalContainer;
333
+ const target = typeof container === "function" ? container() : container && "current" in container ? container.current : container;
334
+ return target ?? void 0;
335
+ }
329
336
 
330
337
  // packages/quassel/src/useChatAction.ts
331
338
  import { useEffect, useRef, useState } from "react";
@@ -506,7 +513,7 @@ function isTableDivider(line) {
506
513
  import { useMemo as useMemo2 } from "react";
507
514
  import { SparklesIcon, WrenchIcon, XIcon } from "lucide-react";
508
515
  import { jsx as jsx9, jsxs as jsxs2 } from "react/jsx-runtime";
509
- var preClasses = "qsl:rounded-lg qsl:bg-secondary qsl:p-3 qsl:font-mono qsl:text-[11px] qsl:leading-[1.625] qsl:break-all qsl:whitespace-pre-wrap";
516
+ var preClasses = "qsl:rounded-lg qsl:bg-secondary qsl:p-3 qsl:font-mono qsl:text-trace qsl:leading-[1.625] qsl:break-all qsl:whitespace-pre-wrap";
510
517
  function StepPopover({
511
518
  message,
512
519
  position,
@@ -519,6 +526,7 @@ function StepPopover({
519
526
  contextElement: document.documentElement
520
527
  }), [position]);
521
528
  const { Button: Button2, Popover: Popover2, PopoverContent: PopoverContent2 } = useQuasselComponents();
529
+ const portalContainer = useQuasselPortalContainer();
522
530
  const tool = message.tool;
523
531
  const thinking = message.role === "thinking";
524
532
  const title = thinking ? texts.thinkingTitle : texts.toolTitle;
@@ -531,10 +539,11 @@ function StepPopover({
531
539
  anchor,
532
540
  "aria-label": title,
533
541
  className: cn(
534
- "qsl:w-[min(680px,calc(100vw-32px))] qsl:gap-0 qsl:overflow-hidden qsl:p-0 qsl:text-[13px] qsl:shadow-pop",
542
+ "qsl:w-[min(680px,calc(100vw-32px))] qsl:gap-0 qsl:overflow-hidden qsl:p-0 qsl:text-[13px] qsl:leading-normal qsl:shadow-pop",
535
543
  thinking ? "qsl:max-h-none" : "qsl:max-h-[min(70vh,620px)]"
536
544
  ),
537
545
  collisionPadding: 16,
546
+ portalContainer,
538
547
  side: "bottom",
539
548
  sideOffset: 10,
540
549
  children: [
@@ -554,13 +563,13 @@ function StepPopover({
554
563
  }
555
564
  function PopoverSection({ label, value, error = false }) {
556
565
  return /* @__PURE__ */ jsxs2("section", { children: [
557
- /* @__PURE__ */ jsx9("h3", { className: "qsl:mb-1.5 qsl:text-[11px] qsl:font-semibold qsl:tracking-[0.04em] qsl:text-muted-foreground qsl:uppercase", children: label }),
566
+ /* @__PURE__ */ jsx9("h3", { className: "qsl:mb-1.5 qsl:text-trace qsl:font-semibold qsl:tracking-[0.04em] qsl:text-muted-foreground qsl:uppercase", children: label }),
558
567
  /* @__PURE__ */ jsx9("pre", { className: cn(preClasses, error && "qsl:text-destructive"), children: value })
559
568
  ] });
560
569
  }
561
570
 
562
571
  // packages/quassel/src/PendingActionCard.tsx
563
- import { CheckIcon as CheckIcon2 } from "lucide-react";
572
+ import { CheckIcon as CheckIcon2, XIcon as XIcon2 } from "lucide-react";
564
573
  import { jsx as jsx10, jsxs as jsxs3 } from "react/jsx-runtime";
565
574
  var cardClasses = "qsl:gap-2.5 qsl:bg-background qsl:px-3.5 qsl:text-sm";
566
575
  var resultText = (result) => typeof result === "string" ? result : result === null || result === void 0 ? "" : JSON.stringify(result);
@@ -577,7 +586,7 @@ function PendingActionCard({
577
586
  return /* @__PURE__ */ jsxs3(Card2, { className: cardClasses, size: "sm", children: [
578
587
  /* @__PURE__ */ jsx10("div", { className: "qsl:leading-normal", children: text }),
579
588
  /* @__PURE__ */ jsxs3("div", { className: "qsl:flex qsl:items-center qsl:gap-1.5 qsl:text-sm qsl:text-muted-foreground", "data-action": "resolved", children: [
580
- /* @__PURE__ */ jsx10(CheckIcon2, { className: "qsl:text-success", size: 12 }),
589
+ action.status === "approved" ? /* @__PURE__ */ jsx10(CheckIcon2, { className: "qsl:text-success", size: 12 }) : /* @__PURE__ */ jsx10(XIcon2, { size: 12 }),
581
590
  action.status === "approved" ? resultText(action.result) : dismissedLabel
582
591
  ] })
583
592
  ] });
@@ -1005,7 +1014,7 @@ function announce(text) {
1005
1014
  import { Fragment as Fragment2, jsx as jsx13, jsxs as jsxs6 } from "react/jsx-runtime";
1006
1015
  var stepClasses = "qsl:mt-[var(--qsl-chat-message-gap,16px)]";
1007
1016
  var denseStepClasses = "qsl:mt-[var(--qsl-chat-dense-message-gap,8px)]";
1008
- var traceClasses = "qsl:flex qsl:w-full qsl:gap-2 qsl:pl-6 qsl:text-left qsl:text-[11px] qsl:leading-[1.375] qsl:text-muted-foreground";
1017
+ var traceClasses = "qsl:flex qsl:w-full qsl:gap-2 qsl:pl-6 qsl:text-left qsl:text-trace qsl:leading-[1.375] qsl:text-muted-foreground";
1009
1018
  var traceButtonClasses = "qsl:cursor-pointer qsl:rounded-md qsl:py-1 qsl:pr-2 qsl:transition-colors qsl:hover:bg-secondary qsl:hover:text-foreground qsl:focus-visible:bg-secondary qsl:focus-visible:text-foreground qsl:focus-visible:outline-none";
1010
1019
  var traceIconClasses = "qsl:mt-0.5 qsl:flex-none qsl:opacity-60";
1011
1020
  var traceErrorIconClasses = "qsl:mt-0.5 qsl:flex-none qsl:text-destructive qsl:opacity-100";
@@ -1013,7 +1022,7 @@ var traceLineClasses = "qsl:min-w-0 qsl:self-baseline qsl:font-mono qsl:[overflo
1013
1022
  var traceRunningClasses = "qsl:ml-1.5 qsl:opacity-60";
1014
1023
  var chipClasses = cn(
1015
1024
  "qsl:inline-flex qsl:max-w-[220px] qsl:items-center qsl:gap-[5px] qsl:rounded-full qsl:border qsl:border-border-soft qsl:px-2.5 qsl:py-0.5",
1016
- "qsl:font-mono qsl:text-[10.5px] qsl:leading-[1.6] qsl:text-muted-foreground qsl:transition-colors",
1025
+ "qsl:font-mono qsl:text-meta qsl:leading-[1.6] qsl:text-muted-foreground qsl:transition-colors",
1017
1026
  "qsl:data-[state=running]:border-[color-mix(in_srgb,var(--qsl-primary)_40%,var(--qsl-border-soft))]",
1018
1027
  "qsl:data-[state=thinking]:border-[color-mix(in_srgb,var(--qsl-primary)_40%,var(--qsl-border-soft))]",
1019
1028
  "qsl:data-[state=error]:border-[color-mix(in_srgb,var(--qsl-destructive)_45%,var(--qsl-border-soft))]",
@@ -1044,6 +1053,7 @@ function ChatMessages({
1044
1053
  onLinkClick,
1045
1054
  emptyState,
1046
1055
  className,
1056
+ scrollerClassName,
1047
1057
  showTimestamps = false,
1048
1058
  announce: announce2,
1049
1059
  scrollerRef,
@@ -1153,7 +1163,7 @@ function ChatMessages({
1153
1163
  );
1154
1164
  const mitZeit = (inhalt, key, at, dicht = false) => showTimestamps ? /* @__PURE__ */ jsxs6("div", { className: cn("qsl:flex qsl:items-baseline qsl:gap-2", dicht ? denseStepClasses : stepClasses), children: [
1155
1165
  /* @__PURE__ */ jsx13("time", { dateTime: messageDate(at)?.toISOString(), className: cn(
1156
- "qsl:flex-none qsl:text-[10.5px] qsl:tabular-nums qsl:text-muted-foreground qsl:opacity-85",
1166
+ "qsl:flex-none qsl:text-meta qsl:tabular-nums qsl:text-muted-foreground qsl:opacity-85",
1157
1167
  timestampOptions?.format && timestampOptions.format !== "time" ? "qsl:w-[clamp(60px,20%,120px)]" : "qsl:w-[34px]"
1158
1168
  ), children: messageDate(at) ? timestampLabel(messageDate(at), timestampOptions, now) : "" }),
1159
1169
  /* @__PURE__ */ jsx13("div", { className: "qsl:min-w-0 qsl:flex-1 qsl:[&>*:first-child]:mt-0", children: inhalt })
@@ -1259,8 +1269,10 @@ function ChatMessages({
1259
1269
  "qsl:[--qsl-fade-edge:calc(100%_-_var(--qsl-composer-height,0px))]",
1260
1270
  "qsl:[-webkit-mask-image:linear-gradient(to_bottom,#000_calc(var(--qsl-fade-edge)_-_40px),transparent_calc(var(--qsl-fade-edge)_+_16px))]",
1261
1271
  "qsl:[mask-image:linear-gradient(to_bottom,#000_calc(var(--qsl-fade-edge)_-_40px),transparent_calc(var(--qsl-fade-edge)_+_16px))]",
1262
- "qsl:in-data-[tone=material]:p-0"
1272
+ "qsl:in-data-[tone=material]:p-0",
1273
+ scrollerClassName
1263
1274
  ),
1275
+ "data-quassel-transcript": "",
1264
1276
  onScroll: pruefeEnde,
1265
1277
  onWheel: (event) => {
1266
1278
  if (!event.ctrlKey && event.deltaY < 0) pauseUp(event.currentTarget, event.target);
@@ -1508,9 +1520,9 @@ function Bubble({
1508
1520
  /* @__PURE__ */ jsxs6("div", { className: "qsl:min-w-0 qsl:flex-1", children: [
1509
1521
  /* @__PURE__ */ jsx13("div", { className: cn(traceLineClasses, thinking && "qsl:font-sans qsl:italic qsl:whitespace-pre-wrap"), children: zeile }),
1510
1522
  tool && /* @__PURE__ */ jsxs6("div", { className: "qsl:mt-0.5 qsl:border-l-2 qsl:border-border-soft qsl:pl-2", children: [
1511
- /* @__PURE__ */ jsx13("pre", { className: "qsl:mb-0.5 qsl:max-h-[208px] qsl:overflow-auto qsl:font-mono qsl:text-[10.5px] qsl:leading-[1.375] qsl:break-all qsl:whitespace-pre-wrap qsl:opacity-80", children: toolArgumentsText(tool) }),
1523
+ /* @__PURE__ */ jsx13("pre", { className: "qsl:mb-0.5 qsl:max-h-[208px] qsl:overflow-auto qsl:font-mono qsl:text-meta qsl:leading-[1.375] qsl:break-all qsl:whitespace-pre-wrap qsl:opacity-80", children: toolArgumentsText(tool) }),
1512
1524
  tool.result !== void 0 && /* @__PURE__ */ jsx13("pre", { className: cn(
1513
- "qsl:mb-0.5 qsl:max-h-[208px] qsl:overflow-auto qsl:font-mono qsl:text-[10.5px] qsl:leading-[1.375] qsl:break-all qsl:whitespace-pre-wrap qsl:opacity-80",
1525
+ "qsl:mb-0.5 qsl:max-h-[208px] qsl:overflow-auto qsl:font-mono qsl:text-meta qsl:leading-[1.375] qsl:break-all qsl:whitespace-pre-wrap qsl:opacity-80",
1514
1526
  tool.isError && "qsl:text-destructive qsl:opacity-100"
1515
1527
  ), children: prettyJson(tool.result) })
1516
1528
  ] })
@@ -1545,12 +1557,12 @@ function Bubble({
1545
1557
  const showSender = bubbleOptions?.showSender ?? (!!message.bubble?.label && !plain);
1546
1558
  const content = /* @__PURE__ */ jsxs6(Fragment2, { children: [
1547
1559
  showSender && sender && /* @__PURE__ */ jsx13("span", { className: cn(
1548
- "qsl:mb-[5px] qsl:table qsl:text-[0.72rem] qsl:leading-[1.45] qsl:font-semibold",
1560
+ "qsl:mb-[5px] qsl:table qsl:text-label qsl:leading-[1.45] qsl:font-semibold",
1549
1561
  useBubble && message.bubble && "qsl:rounded-full qsl:border qsl:border-white/30 qsl:bg-black/15 qsl:px-[7px] qsl:py-px qsl:tracking-[0.02em]"
1550
1562
  ), "data-chat": "sender", children: sender }),
1551
1563
  /* @__PURE__ */ jsx13(Markdown, { text: message.text, streaming: message.role === "assistant" && !message.closed }),
1552
1564
  /* @__PURE__ */ jsx13(MessageAttachments, { label: texts.attachments, message }),
1553
- message.steered && /* @__PURE__ */ jsx13("small", { className: "qsl:mt-1 qsl:block qsl:text-[0.72rem] qsl:leading-[1.45] qsl:opacity-70", "data-chat": "steered", children: texts.steered }),
1565
+ message.steered && /* @__PURE__ */ jsx13("small", { className: "qsl:mt-1 qsl:block qsl:text-label qsl:leading-[1.45] qsl:opacity-70", "data-chat": "steered", children: texts.steered }),
1554
1566
  /* @__PURE__ */ jsx13(MessageActions, { message, options: messageActions, texts })
1555
1567
  ] });
1556
1568
  const cursor = message.role === "assistant" && message.textCursor ? JSON.stringify(message.textCursor) : void 0;
@@ -1565,7 +1577,7 @@ function Bubble({
1565
1577
  children: /* @__PURE__ */ jsx13(
1566
1578
  "div",
1567
1579
  {
1568
- className: cn(bubbleBodyClasses, "qsl:group", message.bubble ? "qsl:rounded-lg qsl:text-white qsl:[--qsl-scroll-cover:var(--qsl-primary)]" : "qsl:rounded-[10px_10px_4px_10px] qsl:border qsl:border-border qsl:bg-secondary qsl:text-foreground qsl:[--qsl-scroll-cover:var(--qsl-secondary)] qsl:in-data-[tone=material]:rounded-[9px] qsl:in-data-[tone=material]:border-glass-edge/20 qsl:in-data-[tone=material]:bg-primary/15 qsl:in-data-[tone=material]:text-foreground"),
1580
+ className: cn(bubbleBodyClasses, "qsl:group", message.bubble ? "qsl:rounded-lg qsl:text-white qsl:[--qsl-scroll-cover:var(--qsl-primary)]" : "qsl:rounded-[var(--qsl-bubble-radius,10px_10px_4px_10px)] qsl:border qsl:border-border qsl:bg-secondary qsl:text-foreground qsl:[--qsl-scroll-cover:var(--qsl-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 qsl:in-data-[tone=material]:text-foreground"),
1569
1581
  "data-tone": message.bubble ? "on-color" : void 0,
1570
1582
  style: { background: message.bubble?.color, maxWidth: bubbleOptions?.maxWidth },
1571
1583
  children: content
@@ -1650,7 +1662,7 @@ function ChatPanel({
1650
1662
  }
1651
1663
 
1652
1664
  // packages/quassel/src/ChatInputToolbar.tsx
1653
- import { PaperclipIcon, SendIcon, XIcon as XIcon2 } from "lucide-react";
1665
+ import { PaperclipIcon, SendIcon, XIcon as XIcon3 } from "lucide-react";
1654
1666
 
1655
1667
  // packages/quassel/src/ui/useFileInput.ts
1656
1668
  import { useRef as useRef5, useState as useState6 } from "react";
@@ -1851,7 +1863,7 @@ function ChatInputToolbar({
1851
1863
  const inputRows = inline || toolbar ? 1 : rows;
1852
1864
  const inputMaxRows = inline || toolbar ? 1 : maxRows;
1853
1865
  const rootClasses = cn(
1854
- "qsl:flex qsl:shrink-0 qsl:flex-col qsl:overflow-hidden qsl:rounded-[var(--qsl-input-card-radius,var(--qsl-radius-xl))] qsl:border qsl:border-border-strong qsl:bg-background qsl:shadow-bar qsl:transition-colors qsl:focus-within:border-primary",
1866
+ "qsl:flex qsl:shrink-0 qsl:flex-col qsl:overflow-hidden qsl:rounded-[var(--qsl-input-card-radius,--theme(--radius-xl))] qsl:border qsl:border-border-strong qsl:bg-background qsl:shadow-bar qsl:transition-colors qsl:focus-within:border-primary",
1855
1867
  "qsl:in-data-[tone=material]:rounded-xl qsl:in-data-[tone=material]:border-glass-edge/50 qsl:in-data-[tone=material]:bg-background",
1856
1868
  toolbar && "qsl:h-full qsl:min-w-0 qsl:flex-row qsl:items-center qsl:rounded-lg qsl:data-[working=true]:animate-working-pulse qsl:data-[working=true]:border-primary qsl:motion-reduce:data-[working=true]:animate-none",
1857
1869
  fileInput.dragging && "qsl:outline-2 qsl:outline-offset-[3px] qsl:outline-dashed qsl:outline-primary"
@@ -2041,7 +2053,7 @@ function ChatInputToolbar({
2041
2053
  size: "icon-sm",
2042
2054
  title: fillText(alleTexte.removeAttachment, { name: entry.file.name }),
2043
2055
  variant: "outline",
2044
- children: /* @__PURE__ */ jsx15(XIcon2, {})
2056
+ children: /* @__PURE__ */ jsx15(XIcon3, {})
2045
2057
  }
2046
2058
  )
2047
2059
  ] }, entry.id)) });
@@ -2291,6 +2303,7 @@ export {
2291
2303
  timestampLabel,
2292
2304
  useChatAction,
2293
2305
  useQuasselComponents,
2306
+ useQuasselPortalContainer,
2294
2307
  validateAttachmentSelection
2295
2308
  };
2296
2309
  //# sourceMappingURL=index.js.map