quassel 0.2.1 → 0.3.0

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.
@@ -0,0 +1,2 @@
1
+ /** Ziel offener Aktionen: undefined = im Verlauf, null = das Dock über der Eingabe ist noch nicht eingehängt. */
2
+ export declare const ChatActionDock: import("react").Context<HTMLElement | null | undefined>;
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, toolElapsedThreshold, }: {
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. */
@@ -41,4 +43,6 @@ export declare function ChatMessages({ messages, owner, detailMode, running, wor
41
43
  codeBlockOptions?: CodeBlockOptions;
42
44
  bubbleOptions?: BubbleOptions;
43
45
  messageActions?: MessageActionsOptions;
46
+ /** Milliseconds before a running tool call shows its elapsed time (from Message.at) while running; false = never. */
47
+ toolElapsedThreshold?: number | false;
44
48
  }): import("react").JSX.Element;
@@ -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
 
@@ -12,12 +12,15 @@ color, radius and font is a `--qsl-*` variable.
12
12
 
13
13
  **Agent chats that show their work.** Thinking and tool calls appear as steps. Pick how much
14
14
  of them the user sees - only the current step, icons, chips, a collapsible group, single lines
15
- or everything - and let them switch at runtime.
15
+ or everything - and let them switch at runtime. A tool call that runs longer than a few seconds
16
+ shows how long it has been running, on its row and on the collapsed group header, so a slow step
17
+ does not look like a hung one.
16
18
 
17
19
  ![The same steps as icons, chips and single lines](https://raw.githubusercontent.com/SchlenkR/quassel/main/docs/images/details.png)
18
20
 
19
21
  **Pending actions.** The agent waits for input: a generic card with dismiss, or the host's own
20
- rendering per action owner. Resolved actions stay in the transcript as a record.
22
+ rendering per action owner. Open actions sit right above the input, like a question in a terminal
23
+ agent; resolved actions stay in the transcript as a record.
21
24
 
22
25
  **Multi-party conversations.** Colored bubbles with labels for agents that talk to each other.
23
26
 
@@ -27,7 +30,9 @@ rendering per action owner. Resolved actions stay in the transcript as a record.
27
30
  </p>
28
31
 
29
32
  **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.
33
+ with `data-theme`, or hand quassel your own Button, Toggle, Card and Popover components. With
34
+ `portalContainer` on `QuasselProvider`, popovers render inside your wrapper and pick up its
35
+ variables too.
31
36
 
32
37
  ![Default, sunset and emerald themes](https://raw.githubusercontent.com/SchlenkR/quassel/main/docs/images/themes.png)
33
38
 
@@ -87,6 +92,7 @@ Any backend that emits the `quassel/events` stream works with the same component
87
92
  pnpm install
88
93
  pnpm dev # stylesheet in watch mode plus the gallery on http://localhost:3210
89
94
  pnpm check # type check package and gallery
95
+ pnpm test # package tests (node:test with happy-dom)
90
96
  pnpm build # stylesheet and gallery build
91
97
  pnpm release --dry-run
92
98
  ```
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;
@@ -86,6 +87,9 @@
86
87
  .qsl\:z-\[110\] {
87
88
  z-index: 110;
88
89
  }
90
+ .qsl\:-mx-\[16px\] {
91
+ margin-inline: calc(16px * -1);
92
+ }
89
93
  .qsl\:mx-auto {
90
94
  margin-inline: auto;
91
95
  }
@@ -199,6 +203,9 @@
199
203
  .qsl\:max-h-\[240px\] {
200
204
  max-height: 240px;
201
205
  }
206
+ .qsl\:max-h-\[calc\(var\(--qsl-panel-height\,100dvh\)_\/_2\)\] {
207
+ max-height: calc(var(--qsl-panel-height,100dvh) / 2);
208
+ }
202
209
  .qsl\:max-h-\[min\(70vh\,620px\)\] {
203
210
  max-height: min(70vh, 620px);
204
211
  }
@@ -376,6 +383,9 @@
376
383
  .qsl\:gap-\[5px\] {
377
384
  gap: 5px;
378
385
  }
386
+ .qsl\:gap-\[var\(--qsl-chat-dense-message-gap\,8px\)\] {
387
+ gap: var(--qsl-chat-dense-message-gap,8px);
388
+ }
379
389
  :where(.qsl\:space-y-0 > :not(:last-child)) {
380
390
  --tw-space-y-reverse: 0;
381
391
  margin-block-start: 0;
@@ -405,32 +415,32 @@
405
415
  .qsl\:overflow-y-auto {
406
416
  overflow-y: auto;
407
417
  }
408
- .qsl\:rounded-\[10px_10px_4px_10px\] {
409
- border-radius: 10px 10px 4px 10px;
418
+ .qsl\:rounded-\[min\(--theme\(--radius-md\)\,10px\)\] {
419
+ border-radius: min(var(--qsl-radius-md, calc(var(--qsl-radius) * 0.8)), 10px);
410
420
  }
411
- .qsl\:rounded-\[min\(var\(--qsl-radius-md\)\,10px\)\] {
412
- border-radius: min(var(--qsl-radius-md), 10px);
421
+ .qsl\:rounded-\[min\(--theme\(--radius-md\)\,12px\)\] {
422
+ border-radius: min(var(--qsl-radius-md, calc(var(--qsl-radius) * 0.8)), 12px);
413
423
  }
414
- .qsl\:rounded-\[min\(var\(--qsl-radius-md\)\,12px\)\] {
415
- border-radius: min(var(--qsl-radius-md), 12px);
424
+ .qsl\:rounded-\[var\(--qsl-bubble-radius\,10px_10px_4px_10px\)\] {
425
+ border-radius: var(--qsl-bubble-radius,10px 10px 4px 10px);
416
426
  }
417
- .qsl\:rounded-\[var\(--qsl-input-card-radius\,var\(--qsl-radius-xl\)\)\] {
418
- border-radius: var(--qsl-input-card-radius,var(--qsl-radius-xl));
427
+ .qsl\:rounded-\[var\(--qsl-input-card-radius\,--theme\(--radius-xl\)\)\] {
428
+ border-radius: var(--qsl-input-card-radius,var(--qsl-radius-xl, calc(var(--qsl-radius) * 1.4)));
419
429
  }
420
430
  .qsl\:rounded-full {
421
431
  border-radius: calc(infinity * 1px);
422
432
  }
423
433
  .qsl\:rounded-lg {
424
- border-radius: var(--qsl-radius);
434
+ border-radius: var(--qsl-radius-lg, var(--qsl-radius));
425
435
  }
426
436
  .qsl\:rounded-md {
427
- border-radius: calc(var(--qsl-radius) * 0.8);
437
+ border-radius: var(--qsl-radius-md, calc(var(--qsl-radius) * 0.8));
428
438
  }
429
439
  .qsl\:rounded-panel {
430
440
  border-radius: var(--qsl-radius-panel);
431
441
  }
432
442
  .qsl\:rounded-sm {
433
- border-radius: calc(var(--qsl-radius) * 0.6);
443
+ border-radius: var(--qsl-radius-sm, calc(var(--qsl-radius) * 0.6));
434
444
  }
435
445
  .qsl\:border {
436
446
  border-style: var(--tw-border-style);
@@ -522,6 +532,9 @@
522
532
  .qsl\:bg-\[linear-gradient\(to_right\,var\(--qsl-scroll-cover\,var\(--qsl-background\)\)_50\%\,transparent\)_left\/20px_100\%_no-repeat_local\,linear-gradient\(to_left\,var\(--qsl-scroll-cover\,var\(--qsl-background\)\)_50\%\,transparent\)_right\/20px_100\%_no-repeat_local\,linear-gradient\(to_right\,\#00000047\,transparent\)_left\/14px_100\%_no-repeat_scroll\,linear-gradient\(to_left\,\#00000047\,transparent\)_right\/14px_100\%_no-repeat_scroll\] {
523
533
  background-image: linear-gradient(to right,var(--qsl-scroll-cover,var(--qsl-background)) 50%,transparent) left/20px 100% no-repeat local,linear-gradient(to left,var(--qsl-scroll-cover,var(--qsl-background)) 50%,transparent) right/20px 100% no-repeat local,linear-gradient(to right,#00000047,transparent) left/14px 100% no-repeat scroll,linear-gradient(to left,#00000047,transparent) right/14px 100% no-repeat scroll;
524
534
  }
535
+ .qsl\:\[mask-image\:linear-gradient\(to_bottom\,\#000_calc\(100\%_-_var\(--qsl-chat-message-gap\,16px\)\)\,transparent\)\] {
536
+ mask-image: linear-gradient(to bottom,#000 calc(100% - var(--qsl-chat-message-gap,16px)),transparent);
537
+ }
525
538
  .qsl\:\[mask-image\:linear-gradient\(to_bottom\,\#000_calc\(var\(--qsl-fade-edge\)_-_40px\)\,transparent_calc\(var\(--qsl-fade-edge\)_\+_16px\)\)\] {
526
539
  mask-image: linear-gradient(to bottom,#000 calc(var(--qsl-fade-edge) - 40px),transparent calc(var(--qsl-fade-edge) + 16px));
527
540
  }
@@ -570,6 +583,9 @@
570
583
  .qsl\:px-\[7px\] {
571
584
  padding-inline: 7px;
572
585
  }
586
+ .qsl\:px-\[16px\] {
587
+ padding-inline: 16px;
588
+ }
573
589
  .qsl\:px-\[var\(--qsl-chat-horizontal-padding\,24px\)\] {
574
590
  padding-inline: var(--qsl-chat-horizontal-padding,24px);
575
591
  }
@@ -621,6 +637,9 @@
621
637
  .qsl\:pb-\[calc\(var\(--qsl-composer-height\,0px\)_\+_max\(3\.25em\,40px\)\)\] {
622
638
  padding-bottom: calc(var(--qsl-composer-height,0px) + max(3.25em, 40px));
623
639
  }
640
+ .qsl\:pb-\[var\(--qsl-chat-message-gap\,16px\)\] {
641
+ padding-bottom: var(--qsl-chat-message-gap,16px);
642
+ }
624
643
  .qsl\:pl-1 {
625
644
  padding-left: var(--qsl-spacing);
626
645
  }
@@ -667,15 +686,6 @@
667
686
  .qsl\:text-\[0\.8rem\] {
668
687
  font-size: 0.8rem;
669
688
  }
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
689
  .qsl\:text-\[13px\] {
680
690
  font-size: 13px;
681
691
  }
@@ -685,6 +695,15 @@
685
695
  .qsl\:text-\[length\:var\(--qsl-chat-font-size\,13px\)\] {
686
696
  font-size: var(--qsl-chat-font-size,13px);
687
697
  }
698
+ .qsl\:text-label {
699
+ font-size: var(--qsl-text-label);
700
+ }
701
+ .qsl\:text-meta {
702
+ font-size: var(--qsl-text-meta);
703
+ }
704
+ .qsl\:text-trace {
705
+ font-size: var(--qsl-text-trace);
706
+ }
688
707
  .qsl\:leading-\[1\.3\] {
689
708
  --tw-leading: 1.3;
690
709
  line-height: 1.3;
@@ -915,6 +934,9 @@
915
934
  .qsl\:\[--qsl-scroll-cover\:var\(--qsl-secondary\)\] {
916
935
  --qsl-scroll-cover: var(--qsl-secondary);
917
936
  }
937
+ .qsl\:\[-webkit-mask-image\:linear-gradient\(to_bottom\,\#000_calc\(100\%_-_var\(--qsl-chat-message-gap\,16px\)\)\,transparent\)\] {
938
+ -webkit-mask-image: linear-gradient(to bottom,#000 calc(100% - var(--qsl-chat-message-gap,16px)),transparent);
939
+ }
918
940
  .qsl\:\[-webkit-mask-image\:linear-gradient\(to_bottom\,\#000_calc\(var\(--qsl-fade-edge\)_-_40px\)\,transparent_calc\(var\(--qsl-fade-edge\)_\+_16px\)\)\] {
919
941
  -webkit-mask-image: linear-gradient(to bottom,#000 calc(var(--qsl-fade-edge) - 40px),transparent calc(var(--qsl-fade-edge) + 16px));
920
942
  }
@@ -950,6 +972,9 @@
950
972
  .qsl\:placeholder\:text-muted-foreground::placeholder {
951
973
  color: var(--qsl-muted-foreground);
952
974
  }
975
+ .qsl\:empty\:hidden:empty {
976
+ display: none;
977
+ }
953
978
  .qsl\:focus-within\:pointer-events-auto:focus-within {
954
979
  pointer-events: auto;
955
980
  }
@@ -1071,13 +1096,13 @@
1071
1096
  display: none;
1072
1097
  }
1073
1098
  :where([data-slot="button-group"]) .qsl\:in-data-\[slot\=button-group\]\:rounded-lg {
1074
- border-radius: var(--qsl-radius);
1099
+ border-radius: var(--qsl-radius-lg, var(--qsl-radius));
1075
1100
  }
1076
- :where([data-tone="material"]) .qsl\:in-data-\[tone\=material\]\:rounded-\[9px\] {
1077
- border-radius: 9px;
1101
+ :where([data-tone="material"]) .qsl\:in-data-\[tone\=material\]\:rounded-\[var\(--qsl-bubble-radius\,9px\)\] {
1102
+ border-radius: var(--qsl-bubble-radius,9px);
1078
1103
  }
1079
1104
  :where([data-tone="material"]) .qsl\:in-data-\[tone\=material\]\:rounded-xl {
1080
- border-radius: calc(var(--qsl-radius) * 1.4);
1105
+ border-radius: var(--qsl-radius-xl, calc(var(--qsl-radius) * 1.4));
1081
1106
  }
1082
1107
  :where([data-tone="material"]) .qsl\:in-data-\[tone\=material\]\:border-glass-edge\/20 {
1083
1108
  border-color: var(--qsl-glass-edge);
@@ -1512,12 +1537,12 @@
1512
1537
  background-color: var(--qsl-border);
1513
1538
  }
1514
1539
  :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);
1540
+ border-top-left-radius: var(--qsl-radius-xl, calc(var(--qsl-radius) * 1.4));
1541
+ border-top-right-radius: var(--qsl-radius-xl, calc(var(--qsl-radius) * 1.4));
1517
1542
  }
1518
1543
  :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);
1544
+ border-bottom-right-radius: var(--qsl-radius-xl, calc(var(--qsl-radius) * 1.4));
1545
+ border-bottom-left-radius: var(--qsl-radius-xl, calc(var(--qsl-radius) * 1.4));
1521
1546
  }
1522
1547
  .qsl\:\[\&\>\*\:first-child\]\:mt-0 > :first-child {
1523
1548
  margin-top: 0px;
@@ -1656,10 +1681,6 @@
1656
1681
  --qsl-pop-shadow: 0 12px 36px #49384e26;
1657
1682
  --qsl-card-shadow: 0 12px 22px #49384e2c;
1658
1683
  }
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
1684
  :where([data-theme="dark"]) {
1664
1685
  --qsl-color-scheme: dark;
1665
1686
  --qsl-background: #292735;
@@ -1750,7 +1771,7 @@
1750
1771
  font-size: 1em;
1751
1772
  }
1752
1773
  :where([data-quassel], [data-quassel] *):where(small) {
1753
- font-size: 80%;
1774
+ font-size: var(--qsl-text-small, 80%);
1754
1775
  }
1755
1776
  :where([data-quassel], [data-quassel] *):where(sub, sup) {
1756
1777
  font-size: 75%;
package/elapsed.d.ts ADDED
@@ -0,0 +1,16 @@
1
+ import { type ChatTexts } from "./texts";
2
+ /** Under a minute "{seconds} s" from texts, then m:ss, from an hour on h:mm:ss. */
3
+ export declare function formatElapsed(ms: number, texts: Pick<ChatTexts, "toolElapsedSeconds">): string;
4
+ /** One interval for every subscriber; it runs only while someone listens. */
5
+ export declare function createTicker(intervalMs?: number): {
6
+ subscribe(listener: () => void): () => void;
7
+ now(): number;
8
+ active: () => boolean;
9
+ };
10
+ export declare const elapsedTicker: {
11
+ subscribe(listener: () => void): () => void;
12
+ now(): number;
13
+ active: () => boolean;
14
+ };
15
+ /** Milliseconds since start in whole seconds, so a row renders only when its display changes; undefined on the server. */
16
+ export declare function useElapsed(start: number): number | undefined;
package/index.d.ts CHANGED
@@ -17,7 +17,8 @@ export { useChatAction, copyChatText } from "./useChatAction";
17
17
  export { createChatScroll } from "./chat-scroll";
18
18
  export { ChatSendContext, createChatSendScope, type ChatSendScope } from "./ChatSendContext";
19
19
  export { messageDate, timestampDay, timestampLabel } from "./timestamps";
20
+ export { formatElapsed, useElapsed } from "./elapsed";
20
21
  export { attachmentCapabilityError, attachmentDownloadUrl, attachmentMediaType, encodeAttachment, formatAttachmentSize, validateAttachmentSelection, MAX_CHAT_ATTACHMENTS, MAX_CHAT_ATTACHMENT_BYTES, } from "./attachments";
21
22
  export type { ChatAppearance, TimestampOptions, CodeBlockOptions, BubbleOptions, MessageAction, MessageActionsOptions, SendShortcut, } from "./options";
22
23
  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";
24
+ 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";