@schlenkr/quassel 0.0.0-stage → 0.5.1

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
+ /** Target of open actions: undefined = in the transcript, null = the dock above the input is not mounted yet. */
2
+ export declare const ChatActionDock: import("react").Context<HTMLElement | null | undefined>;
@@ -0,0 +1,61 @@
1
+ import { ReactNode, Ref } from "react";
2
+ import { ChatTexts } from "./texts";
3
+ import type { ChatAttachmentInput } from "./events";
4
+ import type { SendShortcut } from "./options";
5
+ /** A toolbar button, declarative: icon, text or both - plus click handler. */
6
+ export interface ToolbarAction {
7
+ icon?: ReactNode;
8
+ label?: string;
9
+ title?: string;
10
+ disabled?: boolean;
11
+ active?: boolean;
12
+ onClick: () => void;
13
+ }
14
+ /** Hosts control the draft from outside, e.g. to insert a template for editing. */
15
+ export interface ChatInputHandle {
16
+ /** Replaces the draft, focuses the input and puts the caret at the end. */
17
+ insert: (text: string) => void;
18
+ focus: () => void;
19
+ reset: () => void;
20
+ submit: (onSubmit?: (text: string, attachments?: ChatAttachmentInput[]) => void | Promise<void>, options?: {
21
+ allowEmpty?: boolean;
22
+ }) => Promise<void>;
23
+ }
24
+ /**
25
+ * The input card: borderless textarea on top, toolbar below a hairline. Custom buttons come
26
+ * declaratively through `actions` or freely through the left/right slots; while work is running
27
+ * the stop button stays visible, and typing shows the send button next to it, which then
28
+ * interjects. `rows` sets the initial height; with `maxRows` the textarea grows with its content
29
+ * up to that number of rows and scrolls after that. `inline` starts with one row and grows the same way.
30
+ */
31
+ export declare function ChatInputToolbar({ onSend, onStop, running, disabled, sendDisabled, layout, detailsContainer, onErrorChange, inputAriaControls, rows, maxRows, actions, toolbarLeft, toolbarRight, texts, handleRef, attachments: attachmentsEnabled, attachmentCapabilities, attachmentCapabilitiesError, onAttachmentsChange, initialValue, onDraftChange, sendShortcut, autoFocus, onAutoFocusSettled, }: {
32
+ onSend: (text: string, attachments?: ChatAttachmentInput[]) => void | Promise<void>;
33
+ onStop?: () => void;
34
+ running?: boolean;
35
+ disabled?: boolean;
36
+ sendDisabled?: boolean;
37
+ layout?: "card" | "toolbar" | "inline";
38
+ detailsContainer?: HTMLElement | null;
39
+ onErrorChange?: (error: string | undefined) => void;
40
+ inputAriaControls?: string;
41
+ rows?: number;
42
+ maxRows?: number;
43
+ actions?: ToolbarAction[];
44
+ toolbarLeft?: ReactNode;
45
+ toolbarRight?: ReactNode;
46
+ texts?: Partial<ChatTexts>;
47
+ handleRef?: Ref<ChatInputHandle>;
48
+ /** false = no attaching: neither button nor dropping or pasting files. */
49
+ attachments?: boolean;
50
+ attachmentCapabilities?: {
51
+ input: readonly string[];
52
+ model: string;
53
+ };
54
+ attachmentCapabilitiesError?: string;
55
+ onAttachmentsChange?: (hasAttachments: boolean) => void;
56
+ initialValue?: string;
57
+ onDraftChange?: (text: string) => void;
58
+ sendShortcut?: SendShortcut;
59
+ autoFocus?: boolean;
60
+ onAutoFocusSettled?: () => void;
61
+ }): import("react").JSX.Element;
@@ -0,0 +1,51 @@
1
+ import { type CSSProperties, ReactNode } from "react";
2
+ import { ChatAnnouncement, DetailMode, Message, PendingAction, ToolInfo, TranscriptMode } from "./types";
3
+ import { ChatTexts } from "./texts";
4
+ import { type LinkClickHandler } from "./Markdown";
5
+ import { type ChatAppearance, type TimestampOptions, type CodeBlockOptions, type BubbleOptions, type MessageActionsOptions } from "./options";
6
+ /**
7
+ * The transcript: its own scroll container with autoscroll while you are at the bottom, and a
8
+ * jump-to-end button as soon as you are reading. Usable read-only without a composer.
9
+ */
10
+ export declare function ChatMessages({ messages, owner, detailMode, transcriptMode, running, working, stepsExpandable, groupsExpandable, texts, toolArgumentsText, renderTool, renderAction, onDismissAction, onLinkClick, emptyState, className, scrollerClassName, showTimestamps, announce, scrollerRef, maxWidth, horizontalPadding, jumpToEndThreshold, appearance, timestampOptions, codeBlockOptions, bubbleOptions, messageActions, toolElapsedThreshold, }: {
11
+ messages: Message[];
12
+ /** Contributions with this Message.sender appear without a speech bubble; null keeps the message defaults. */
13
+ owner?: string | null;
14
+ detailMode?: DetailMode;
15
+ transcriptMode?: TranscriptMode;
16
+ running?: boolean;
17
+ working?: ReactNode;
18
+ /** false = thinking and tool steps cannot be expanded (no popover). */
19
+ stepsExpandable?: boolean;
20
+ /** false = step groups in grouped mode stay collapsed (header without toggle). */
21
+ groupsExpandable?: boolean;
22
+ texts?: Partial<ChatTexts>;
23
+ toolArgumentsText?: (tool: ToolInfo) => string;
24
+ /** Custom rendering for single tools; undefined = default rendering. */
25
+ renderTool?: (tool: ToolInfo) => ReactNode | undefined;
26
+ /** Rendering of a pending action by its owner; undefined = generic card. */
27
+ renderAction?: (action: PendingAction, text: string) => ReactNode | undefined;
28
+ onDismissAction?: (actionId: string) => void;
29
+ /** Intercepts clicks on Markdown links; true = handled, the browser does not follow. */
30
+ onLinkClick?: LinkClickHandler;
31
+ emptyState?: ReactNode;
32
+ className?: string;
33
+ /** Host classes for the scroll container (data-quassel-transcript). */
34
+ scrollerClassName?: string;
35
+ /** true = subtle HH:MM column to the left of each block (Message.at). */
36
+ showTimestamps?: boolean;
37
+ /** Screen reader announcement of finished answers and new actions; false = the host announces itself. */
38
+ announce?: false | ((announcement: ChatAnnouncement) => string | undefined);
39
+ /** Exposes the scroll container so hosts can scroll without querying the DOM. */
40
+ scrollerRef?: (element: HTMLDivElement | null) => void;
41
+ maxWidth?: CSSProperties["maxWidth"];
42
+ horizontalPadding?: CSSProperties["paddingInline"];
43
+ jumpToEndThreshold?: number;
44
+ appearance?: ChatAppearance;
45
+ timestampOptions?: TimestampOptions;
46
+ codeBlockOptions?: CodeBlockOptions;
47
+ bubbleOptions?: BubbleOptions;
48
+ messageActions?: MessageActionsOptions;
49
+ /** Milliseconds before a running tool call shows its elapsed time (from Message.at) while running; false = never. */
50
+ toolElapsedThreshold?: number | false;
51
+ }): import("react").JSX.Element;
package/ChatPanel.d.ts ADDED
@@ -0,0 +1,17 @@
1
+ import { type CSSProperties, type ReactNode, type Ref } from "react";
2
+ import { type ChatAppearance } from "./options";
3
+ /**
4
+ * Transcript with an overlaid composer. The frame measures the composer itself and gives the
5
+ * transcript the footer space it needs, so no host has to set inner dimensions.
6
+ */
7
+ export declare function ChatPanel({ children, composer, className, nodeRef, maxWidth, horizontalPadding, appearance, scrollOnSend, }: {
8
+ children: ReactNode;
9
+ /** The composer; it lies over the transcript, not below it. */
10
+ composer?: ReactNode;
11
+ className?: string;
12
+ nodeRef?: Ref<HTMLDivElement>;
13
+ maxWidth?: CSSProperties["maxWidth"];
14
+ horizontalPadding?: CSSProperties["paddingInline"];
15
+ appearance?: ChatAppearance;
16
+ scrollOnSend?: boolean;
17
+ }): import("react").JSX.Element;
@@ -0,0 +1,7 @@
1
+ import { type RefObject } from "react";
2
+ export interface ChatSendScope {
3
+ notify: () => void;
4
+ subscribe: (listener: () => void) => () => void;
5
+ }
6
+ export declare const ChatSendContext: import("react").Context<ChatSendScope | undefined>;
7
+ export declare function createChatSendScope(enabled: RefObject<boolean>): ChatSendScope;
@@ -0,0 +1,16 @@
1
+ import { DetailMode } from "./types";
2
+ import { ChatTexts } from "./texts";
3
+ export declare const DETAIL_MODES: readonly DetailMode[];
4
+ export declare const detailModeLabel: (mode: DetailMode, texts?: Partial<ChatTexts>) => string;
5
+ /**
6
+ * An icon button that cycles the detail level of the thinking and tool steps. The order comes
7
+ * from `modes`, the current value shows in the icon and in the tooltip.
8
+ */
9
+ export declare function DetailModeSwitch({ mode, onChange, modes, texts, className, }: {
10
+ mode: DetailMode;
11
+ onChange: (mode: DetailMode) => void;
12
+ /** Selection and order of cycling; the default is all modes. */
13
+ modes?: readonly DetailMode[];
14
+ texts?: Partial<ChatTexts>;
15
+ className?: string;
16
+ }): import("react").JSX.Element;
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ronald Schlenker
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/Markdown.d.ts ADDED
@@ -0,0 +1,19 @@
1
+ import { ReactNode } from "react";
2
+ import type { CodeBlockOptions } from "./options";
3
+ import { type ChatTexts } from "./texts";
4
+ export type LinkClickHandler = (href: string, label: string) => boolean;
5
+ export declare function MarkdownCodeBlocks({ children, options, texts }: {
6
+ children: ReactNode;
7
+ options?: CodeBlockOptions;
8
+ texts: ChatTexts;
9
+ }): import("react").JSX.Element;
10
+ export declare function MarkdownLinks({ children, onLinkClick }: {
11
+ children: ReactNode;
12
+ onLinkClick?: LinkClickHandler;
13
+ }): import("react").JSX.Element;
14
+ export declare function Markdown({ text, streaming }: {
15
+ text: string;
16
+ streaming?: boolean;
17
+ }): import("react").JSX.Element;
18
+ /** The same syntax as the renderer, as plain text without markup, e.g. for screen reader announcements. */
19
+ export declare function markdownPlainText(text: string): string;
@@ -0,0 +1,8 @@
1
+ import type { MessageActionsOptions } from "./options";
2
+ import type { ChatTexts } from "./texts";
3
+ import type { Message } from "./types";
4
+ export declare function MessageActions({ message, options, texts }: {
5
+ message: Message;
6
+ options?: MessageActionsOptions;
7
+ texts: ChatTexts;
8
+ }): import("react").JSX.Element;
@@ -0,0 +1,13 @@
1
+ import type { PendingAction } from "./types";
2
+ /**
3
+ * The rendering of a pending action without a contribution from its owner: title, the hint
4
+ * that input is expected, and dismissing. Only the plugin knows the form.
5
+ */
6
+ export declare function PendingActionCard({ text, action, waitingLabel, dismissLabel, dismissedLabel, onDismiss, }: {
7
+ text: string;
8
+ action: PendingAction;
9
+ waitingLabel: string;
10
+ dismissLabel: string;
11
+ dismissedLabel: string;
12
+ onDismiss?: () => void;
13
+ }): import("react").JSX.Element;
@@ -0,0 +1,89 @@
1
+ import { type ComponentType, type CSSProperties, type ReactNode, type RefObject } from "react";
2
+ export type QuasselButtonVariant = "default" | "outline" | "secondary" | "ghost" | "destructive" | "link";
3
+ export type QuasselButtonSize = "default" | "xs" | "sm" | "lg" | "icon" | "icon-xs" | "icon-sm" | "icon-lg";
4
+ export interface QuasselButtonProps {
5
+ variant?: QuasselButtonVariant;
6
+ size?: QuasselButtonSize;
7
+ className?: string;
8
+ disabled?: boolean;
9
+ title?: string;
10
+ "aria-label"?: string;
11
+ "aria-pressed"?: boolean;
12
+ onClick?: () => void;
13
+ children?: ReactNode;
14
+ }
15
+ export interface QuasselToggleProps {
16
+ variant?: "default" | "outline";
17
+ size?: "default" | "sm" | "lg";
18
+ className?: string;
19
+ disabled?: boolean;
20
+ title?: string;
21
+ pressed?: boolean;
22
+ onPressedChange?: (pressed: boolean) => void;
23
+ children?: ReactNode;
24
+ }
25
+ export interface QuasselCardProps {
26
+ size?: "default" | "sm";
27
+ className?: string;
28
+ children?: ReactNode;
29
+ }
30
+ export interface QuasselStopButtonProps {
31
+ label: string;
32
+ busy?: boolean;
33
+ size?: QuasselButtonSize;
34
+ className?: string;
35
+ disabled?: boolean;
36
+ title?: string;
37
+ onClick?: () => void;
38
+ }
39
+ export interface QuasselPopoverProps {
40
+ open?: boolean;
41
+ onOpenChange?: (open: boolean) => void;
42
+ children?: ReactNode;
43
+ }
44
+ export interface QuasselPopoverAnchor {
45
+ getBoundingClientRect: () => DOMRect;
46
+ contextElement?: Element;
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);
51
+ export interface QuasselPopoverContentProps {
52
+ anchor?: QuasselPopoverAnchor;
53
+ align?: "start" | "center" | "end";
54
+ side?: "top" | "bottom" | "left" | "right" | "inline-start" | "inline-end";
55
+ sideOffset?: number;
56
+ collisionPadding?: number;
57
+ className?: string;
58
+ /** Carries --qsl-base, the text size of the chat that opened the popover; pass it on to the popup. */
59
+ style?: CSSProperties;
60
+ "aria-label"?: string;
61
+ /** The resolved portalContainer of QuasselProvider; undefined = document.body. */
62
+ portalContainer?: QuasselPortalTarget;
63
+ children?: ReactNode;
64
+ }
65
+ /** The primitives quassel renders; a host replaces any of them through QuasselProvider. */
66
+ export interface QuasselComponents {
67
+ Button: ComponentType<QuasselButtonProps>;
68
+ Toggle: ComponentType<QuasselToggleProps>;
69
+ Card: ComponentType<QuasselCardProps>;
70
+ StopButton: ComponentType<QuasselStopButtonProps>;
71
+ Popover: ComponentType<QuasselPopoverProps>;
72
+ PopoverContent: ComponentType<QuasselPopoverContentProps>;
73
+ }
74
+ export declare const defaultComponents: QuasselComponents;
75
+ export declare function QuasselProvider({ components, allowUrl, resolveUrl, portalContainer, children }: {
76
+ /** Replaces single primitives; the rest stay the built-in ones or those of an outer provider. */
77
+ components?: Partial<QuasselComponents>;
78
+ /** Keeps Markdown link and image URLs that the default policy (http, https, mailto, tel, ftp, irc, xmpp, relative) would drop. */
79
+ allowUrl?: (url: string) => boolean;
80
+ /** Maps every Markdown link and image URL before the link policy applies, e.g. relative paths onto the host's file route. */
81
+ resolveUrl?: (url: string) => string;
82
+ /** Mounts quassel's popovers inside this element instead of document.body, e.g. within the host's theme wrapper. */
83
+ portalContainer?: QuasselPortalContainer;
84
+ children: ReactNode;
85
+ }): import("react").JSX.Element;
86
+ export declare const useQuasselComponents: () => QuasselComponents;
87
+ export declare const useAllowUrl: () => ((url: string) => boolean) | undefined;
88
+ export declare const useResolveUrl: () => ((url: string) => string) | undefined;
89
+ export declare function useQuasselPortalContainer(): QuasselPortalTarget | undefined;
package/README.md CHANGED
@@ -1,3 +1,136 @@
1
- # Temporary Holding Version
1
+ # quassel
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Chat building blocks for React: a streaming transcript with thinking and tool steps, pending
4
+ actions, an input card with attachments and steering, and a small event contract that works
5
+ on the server as well. quassel is the chat of [RAgents](https://github.com/SchlenkR/RAgents),
6
+ published as a library. It ships a compiled stylesheet, so hosts need no Tailwind, and every
7
+ color, radius, font and font size is a `--qsl-*` variable, with compact defaults for text and tool steps.
8
+
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
+
11
+ ## What you can build
12
+
13
+ **Agent chats that show their work.** Thinking and tool calls appear as steps. Pick how much
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. 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.
18
+
19
+ ![The same steps as icons, chips and single lines](https://raw.githubusercontent.com/SchlenkR/quassel/main/docs/images/details.png)
20
+
21
+ **Pending actions.** The agent waits for input: a generic card with dismiss, or the host's own
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.
24
+
25
+ **Multi-party conversations.** Colored bubbles with labels for agents that talk to each other.
26
+
27
+ <p>
28
+ <img src="docs/images/questions.png" alt="Pending actions: a host-rendered choice and the generic card" width="49%">
29
+ <img src="docs/images/party.png" alt="Planner and reviewer as labeled bubbles" width="49%">
30
+ </p>
31
+
32
+ **Your look, not a fork.** Override `--qsl-*` variables on any wrapper, switch light and dark
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.
36
+
37
+ ![Default, sunset and emerald themes](https://raw.githubusercontent.com/SchlenkR/quassel/main/docs/images/themes.png)
38
+
39
+ ![The agent chat in dark mode](https://raw.githubusercontent.com/SchlenkR/quassel/main/docs/images/agent-dark.png)
40
+
41
+ ## Highlights
42
+
43
+ - Streaming-safe Markdown (tables, code with copy button, lists) via Streamdown.
44
+ - Steering: typing while a turn runs sends into it; stop, retry and failed-send recovery built in.
45
+ - Attachments by button, drag and drop or paste, with previews and size limits.
46
+ - Condensed conversation: all user inputs and the latest answer between them, independently of tool/thinking detail.
47
+ - Timestamps, day separators, message actions, configurable bubbles and appearance.
48
+ - Screen reader friendly: the transcript is not a live region; finished replies and new
49
+ actions are announced once, with the host in control of the wording.
50
+ - Complete English and German UI labels through `englishTexts` / `germanTexts`; `texts` can override any label (English by default).
51
+ - Grouped steps show their error count even while collapsed.
52
+ - Text size follows the host: no overrides needed to fit in (see below).
53
+
54
+ ## Install
55
+
56
+ ```sh
57
+ npm install @schlenkr/quassel
58
+ ```
59
+
60
+ Up to 0.4.9 the package was published without scope as `quassel`; from 0.5.0 on it is `@schlenkr/quassel` with the same API.
61
+
62
+ | Import | Contents |
63
+ |---|---|
64
+ | `@schlenkr/quassel` | React components, `QuasselProvider`, types, texts, `announce`, `markdownPlainText` |
65
+ | `@schlenkr/quassel/events` | The event contract, the pure reducer `applyEvent` and the attachment limits, without React |
66
+ | `@schlenkr/quassel/chat.css` | The compiled stylesheet: theme variables, scoped reset and all utilities quassel uses |
67
+
68
+ ```tsx
69
+ import { useReducer, useState } from "react";
70
+ import { applyEvent, ChatInputToolbar, ChatMessages, ChatPanel, type Message } from "@schlenkr/quassel";
71
+ import "@schlenkr/quassel/chat.css";
72
+
73
+ export function Chat({ send, stop }: { send: (text: string) => Promise<void>; stop: () => void }) {
74
+ const [messages, dispatch] = useReducer(applyEvent, [] as Message[]);
75
+ const [running, setRunning] = useState(false);
76
+ // feed your backend's ChatEvent stream into dispatch(event) and setRunning(event.running) on "status"
77
+ return (
78
+ <ChatPanel composer={<ChatInputToolbar onSend={send} onStop={stop} running={running} rows={1} />}>
79
+ <ChatMessages messages={messages} running={running} detailMode="grouped" />
80
+ </ChatPanel>
81
+ );
82
+ }
83
+ ```
84
+
85
+ ```css
86
+ .my-chat {
87
+ --qsl-primary: #11855c;
88
+ --qsl-radius: 0.9rem;
89
+ }
90
+ ```
91
+
92
+ Any backend that emits the `@schlenkr/quassel/events` stream works with the same components.
93
+
94
+ ## Text size
95
+
96
+ quassel starts at 13px with a line height of 1.5, a 10px message gap and a 3px dense
97
+ step gap. Tool rows and timestamps use 88% of the text size. Expanded tool groups have a
98
+ 1px gap between rows; tool rows have 1px of vertical padding on each side.
99
+
100
+ Every size derives from one base, so nested elements never compound. Set `--qsl-font-size`
101
+ on a wrapper, or `appearance.fontSize`, to change it. Use `1em` to follow the host's font size.
102
+ `appearance` also offers `toolFontScale`, `toolGroupGap` and `toolRowPadding`.
103
+
104
+ User message backgrounds fill the available row by default while text keeps its width and
105
+ alignment. `bubbleOptions={{ userBackgroundFullWidth: false }}` restores a fitted background.
106
+ Timestamps sit beside messages by default; their size follows the tool text unless overridden:
107
+
108
+ ```tsx
109
+ <ChatMessages
110
+ messages={messages}
111
+ showTimestamps
112
+ timestampOptions={{ position: "above", fontScale: 0.88 }}
113
+ />
114
+ ```
115
+
116
+ `--qsl-text-trace`, `--qsl-timestamp-font-size` and the other `--qsl-text-*` variables override
117
+ individual sizes. The [guide](https://github.com/SchlenkR/quassel/blob/main/docs/guide.md#text-size) lists the full API.
118
+
119
+ ## Development
120
+
121
+ ```sh
122
+ pnpm install
123
+ pnpm dev # stylesheet in watch mode plus the gallery on http://localhost:3210
124
+ pnpm check # type check package and gallery
125
+ pnpm test # package tests (node:test with happy-dom)
126
+ pnpm build # stylesheet and gallery build
127
+ pnpm release --dry-run
128
+ ```
129
+
130
+ The full guide - every prop, the event contract, theming, slots, dark mode and publishing -
131
+ is in [docs/guide.md](https://github.com/SchlenkR/quassel/blob/main/docs/guide.md). It is written so that an AI assistant can build
132
+ an application with quassel from it.
133
+
134
+ ## License
135
+
136
+ [MIT](https://github.com/SchlenkR/quassel/blob/main/LICENSE)
@@ -0,0 +1,16 @@
1
+ import { type MouseEvent } from "react";
2
+ import { Message } from "./types";
3
+ import { ChatTexts } from "./texts";
4
+ export type StepPopoverOrigin = {
5
+ x: number;
6
+ y: number;
7
+ base: string;
8
+ };
9
+ export declare const stepPopoverOrigin: (event: MouseEvent<HTMLElement>) => StepPopoverOrigin;
10
+ export declare function StepPopover({ message, position, texts, toolArgumentsText, onClose, }: {
11
+ message: Message;
12
+ position: StepPopoverOrigin;
13
+ texts: ChatTexts;
14
+ toolArgumentsText: (tool: NonNullable<Message["tool"]>) => string;
15
+ onClose: () => void;
16
+ }): import("react").JSX.Element;
@@ -0,0 +1,7 @@
1
+ import { type ChatTexts } from "./texts";
2
+ export declare function TimestampSwitch({ showTimestamps, onChange, texts, className }: {
3
+ showTimestamps: boolean;
4
+ onChange: (showTimestamps: boolean) => void;
5
+ texts?: Partial<ChatTexts>;
6
+ className?: string;
7
+ }): import("react").JSX.Element;
@@ -0,0 +1,8 @@
1
+ import { type TranscriptMode } from "./types";
2
+ import { type ChatTexts } from "./texts";
3
+ export declare function TranscriptModeSwitch({ mode, onChange, texts, className }: {
4
+ mode: TranscriptMode;
5
+ onChange: (mode: TranscriptMode) => void;
6
+ texts?: Partial<ChatTexts>;
7
+ className?: string;
8
+ }): import("react").JSX.Element;
@@ -0,0 +1,12 @@
1
+ export interface WorkingScenesProps {
2
+ /** Limits scenes to one or two visible characters for small status fields. */
3
+ compact?: boolean;
4
+ /** Text for screen readers, default "Working ...". */
5
+ label?: string;
6
+ }
7
+ /**
8
+ * The default working indicator: randomly rotating mini scenes. Random order and start phase,
9
+ * so the same scenes never feel like a loop. The working prop of ChatMessages replaces it
10
+ * completely.
11
+ */
12
+ export declare function WorkingScenes({ label, compact }: WorkingScenesProps): import("react").JSX.Element;
package/announce.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ /** Announces text through a shared, hidden live region: throttled, and paused while the tab is hidden. */
2
+ export declare function announce(text: string): void;
@@ -0,0 +1,24 @@
1
+ import type { ChatAttachmentInput } from "./events";
2
+ import { MAX_CHAT_ATTACHMENTS, MAX_CHAT_ATTACHMENT_BYTES } from "./chat-attachments";
3
+ import { type ChatTexts } from "./texts";
4
+ export { MAX_CHAT_ATTACHMENTS, MAX_CHAT_ATTACHMENT_BYTES };
5
+ export declare const maxAttachmentSizeLabel = "20 MiB";
6
+ export declare function attachmentMediaType(file: {
7
+ name: string;
8
+ type: string;
9
+ }): string;
10
+ export declare function attachmentCapabilityError(files: readonly {
11
+ name: string;
12
+ mediaType: string;
13
+ }[], capabilities: {
14
+ input: readonly string[];
15
+ model: string;
16
+ }, texts?: ChatTexts): string | undefined;
17
+ export declare function validateAttachmentSelection(existing: readonly {
18
+ size: number;
19
+ }[], incoming: readonly {
20
+ size: number;
21
+ }[], texts?: ChatTexts): void;
22
+ export declare function encodeAttachment(file: File): Promise<ChatAttachmentInput>;
23
+ export declare function formatAttachmentSize(bytes: number): string;
24
+ export declare function attachmentDownloadUrl(url: string): string;
@@ -0,0 +1,6 @@
1
+ import type { ChatAttachmentInput } from "./events";
2
+ export declare const MAX_CHAT_ATTACHMENTS = 8;
3
+ export declare const MAX_CHAT_ATTACHMENT_BYTES: number;
4
+ export declare const MAX_CHAT_REQUEST_BYTES: number;
5
+ export declare const chatAttachmentMediaType: (name: string, mediaType: string) => string;
6
+ export declare const parseChatAttachments: (value: unknown) => ChatAttachmentInput[];
@@ -0,0 +1,12 @@
1
+ interface ScrollViewport {
2
+ scrollTop: number;
3
+ readonly scrollHeight: number;
4
+ readonly clientHeight: number;
5
+ }
6
+ export declare function createChatScroll(onFollowChange?: (following: boolean) => void): {
7
+ layout: (viewport: ScrollViewport, dragging?: boolean) => void;
8
+ pause(viewport: ScrollViewport): void;
9
+ scroll(viewport: ScrollViewport, dragging?: boolean): void;
10
+ jump(viewport: ScrollViewport): void;
11
+ };
12
+ export {};