@schlenkr/quassel 0.6.0 → 0.7.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.
- package/ChatInputToolbar.d.ts +6 -10
- package/ComposerPrimaryAction.d.ts +13 -0
- package/QuasselProvider.d.ts +1 -0
- package/README.md +29 -1
- package/RecordingAura.d.ts +4 -0
- package/chat.css +162 -3
- package/index.d.ts +1 -1
- package/index.js +371 -107
- package/index.js.map +4 -4
- package/options.d.ts +12 -0
- package/package.json +1 -1
- package/texts.d.ts +5 -0
package/ChatInputToolbar.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ReactNode, Ref } from "react";
|
|
2
2
|
import { ChatTexts } from "./texts";
|
|
3
3
|
import type { ChatAttachmentInput } from "./events";
|
|
4
|
-
import type { SendShortcut } from "./options";
|
|
4
|
+
import type { ChatMicrophone, SendShortcut } from "./options";
|
|
5
5
|
/** A toolbar button, declarative: icon, text or both - plus click handler. */
|
|
6
6
|
export interface ToolbarAction {
|
|
7
7
|
icon?: ReactNode;
|
|
@@ -21,20 +21,14 @@ export interface ChatInputHandle {
|
|
|
21
21
|
allowEmpty?: boolean;
|
|
22
22
|
}) => Promise<void>;
|
|
23
23
|
}
|
|
24
|
-
/**
|
|
25
|
-
|
|
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, }: {
|
|
24
|
+
/** A composer with attachments, host slots and steering into a running turn. */
|
|
25
|
+
export declare function ChatInputToolbar({ onSend, onStop, running, disabled, sendDisabled, layout, detailsContainer, onErrorChange, inputAriaControls, rows, maxRows, actions, toolbarLeft, toolbarRight, microphone, replaceInput, texts, handleRef, attachments: attachmentsEnabled, attachmentCapabilities, attachmentCapabilitiesError, onAttachmentsChange, initialValue, onDraftChange, sendShortcut, autoFocus, onAutoFocusSettled, }: {
|
|
32
26
|
onSend: (text: string, attachments?: ChatAttachmentInput[]) => void | Promise<void>;
|
|
33
27
|
onStop?: () => void;
|
|
34
28
|
running?: boolean;
|
|
35
29
|
disabled?: boolean;
|
|
36
30
|
sendDisabled?: boolean;
|
|
37
|
-
layout?: "card" | "toolbar" | "inline";
|
|
31
|
+
layout?: "card" | "toolbar" | "inline" | "pill";
|
|
38
32
|
detailsContainer?: HTMLElement | null;
|
|
39
33
|
onErrorChange?: (error: string | undefined) => void;
|
|
40
34
|
inputAriaControls?: string;
|
|
@@ -43,6 +37,8 @@ export declare function ChatInputToolbar({ onSend, onStop, running, disabled, se
|
|
|
43
37
|
actions?: ToolbarAction[];
|
|
44
38
|
toolbarLeft?: ReactNode;
|
|
45
39
|
toolbarRight?: ReactNode;
|
|
40
|
+
microphone?: ChatMicrophone;
|
|
41
|
+
replaceInput?: ReactNode;
|
|
46
42
|
texts?: Partial<ChatTexts>;
|
|
47
43
|
handleRef?: Ref<ChatInputHandle>;
|
|
48
44
|
/** false = no attaching: neither button nor dropping or pasting files. */
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ChatMicrophone } from "./options";
|
|
2
|
+
import type { ChatTexts } from "./texts";
|
|
3
|
+
export declare function ComposerPrimaryAction({ hasContent, running, sending, disabled, sendDisabled, microphone, texts, onSend, onStop }: {
|
|
4
|
+
hasContent: boolean;
|
|
5
|
+
running: boolean;
|
|
6
|
+
sending: boolean;
|
|
7
|
+
disabled: boolean;
|
|
8
|
+
sendDisabled: boolean;
|
|
9
|
+
microphone?: ChatMicrophone;
|
|
10
|
+
texts: Required<ChatTexts>;
|
|
11
|
+
onSend: () => void;
|
|
12
|
+
onStop?: () => void;
|
|
13
|
+
}): import("react").JSX.Element;
|
package/QuasselProvider.d.ts
CHANGED
package/README.md
CHANGED
|
@@ -43,6 +43,7 @@ variables too.
|
|
|
43
43
|
- Streaming-safe Markdown (tables, code with copy button, lists) via Streamdown.
|
|
44
44
|
- Steering: typing while a turn runs sends into it; stop, retry and failed-send recovery built in.
|
|
45
45
|
- Attachments by button, drag and drop or paste, with previews and size limits.
|
|
46
|
+
- Optional pill composer: one animated action for microphone, send, steering and stop, with host-controlled recording and transcription.
|
|
46
47
|
- Condensed conversation: all user inputs and the latest answer between them, independently of tool/thinking detail.
|
|
47
48
|
- Timestamps, day separators, message actions, configurable bubbles and appearance.
|
|
48
49
|
- Screen reader friendly: the transcript is not a live region; finished replies and new
|
|
@@ -91,11 +92,38 @@ export function Chat({ send, stop }: { send: (text: string) => Promise<void>; st
|
|
|
91
92
|
|
|
92
93
|
Any backend that emits the `@schlenkr/quassel/events` stream works with the same components.
|
|
93
94
|
|
|
95
|
+
## Pill composer
|
|
96
|
+
|
|
97
|
+
Use `layout="pill"` for a larger, rounded composer with one 48px primary action. Host buttons
|
|
98
|
+
go into `toolbarLeft` and `toolbarRight`; attachments stay just before the primary action.
|
|
99
|
+
|
|
100
|
+
```tsx
|
|
101
|
+
<ChatInputToolbar
|
|
102
|
+
layout="pill"
|
|
103
|
+
maxRows={5}
|
|
104
|
+
running={running}
|
|
105
|
+
onSend={send}
|
|
106
|
+
onStop={stop}
|
|
107
|
+
microphone={microphone}
|
|
108
|
+
replaceInput={microphone.state === "recording" ? <LiveWaveform /> : undefined}
|
|
109
|
+
toolbarLeft={<ConversationControls />}
|
|
110
|
+
/>
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Empty input shows the microphone, text or attachments show send, and an empty running turn
|
|
114
|
+
shows stop. Typing while running sends into that turn. Without `microphone`, empty input shows
|
|
115
|
+
a disabled send button. The host owns audio, press-and-hold or click-to-latch behavior, and
|
|
116
|
+
transcription through the typed `ChatMicrophone` API. Recording has a live color and an aura
|
|
117
|
+
driven by `level` (0-1); preparing and transcribing show a spinner. The aura renders outside
|
|
118
|
+
clipping ancestors and animations respect reduced motion. Card, inline and toolbar keep their defaults.
|
|
119
|
+
See the [API, state table and microphone integration](https://github.com/SchlenkR/quassel/blob/main/docs/guide.md#pill-composer) and the gallery's Pill composer demo.
|
|
120
|
+
|
|
94
121
|
## Text size
|
|
95
122
|
|
|
96
123
|
quassel defaults to 13px. Set `--qsl-font-size` on a wrapper, or `1em` to inherit the host's
|
|
97
124
|
font size. Messages, the composer, action cards and text buttons use that base with line height
|
|
98
|
-
1.5. All small UI text, including tools and timestamps, uses 85% with
|
|
125
|
+
1.5, the pill input included. All small UI text, including tools and timestamps, uses 85% with
|
|
126
|
+
line height 1.4.
|
|
99
127
|
`--qsl-font` and `--qsl-mono` select the font families. There are no other typography inputs.
|
|
100
128
|
Sizes derive from an absolute base, so nested small elements never compound.
|
|
101
129
|
|
package/chat.css
CHANGED
|
@@ -21,12 +21,57 @@
|
|
|
21
21
|
container-type: inline-size;
|
|
22
22
|
container-name: chat;
|
|
23
23
|
}
|
|
24
|
+
.qsl\:recording-aura {
|
|
25
|
+
position: fixed;
|
|
26
|
+
visibility: hidden;
|
|
27
|
+
pointer-events: none;
|
|
28
|
+
border: 2px solid var(--qsl-pill-aura-ring-color);
|
|
29
|
+
@supports (color: color-mix(in lab, red, red)) {
|
|
30
|
+
border: 2px solid color-mix(in srgb, var(--qsl-pill-aura-ring-color) 55%, transparent);
|
|
31
|
+
}
|
|
32
|
+
border-radius: 50%;
|
|
33
|
+
background: radial-gradient(circle, transparent 42%, var(--qsl-pill-aura-color) 70%, transparent);
|
|
34
|
+
@supports (color: color-mix(in lab, red, red)) {
|
|
35
|
+
background: radial-gradient(circle, transparent 42%, color-mix(in srgb, var(--qsl-pill-aura-color) 30%, transparent) 70%, transparent);
|
|
36
|
+
}
|
|
37
|
+
box-shadow: 0 0 18px 5px var(--qsl-pill-aura-color);
|
|
38
|
+
@supports (color: color-mix(in lab, red, red)) {
|
|
39
|
+
box-shadow: 0 0 18px 5px color-mix(in srgb, var(--qsl-pill-aura-color) 25%, transparent);
|
|
40
|
+
}
|
|
41
|
+
transition: opacity 80ms linear, transform 80ms linear;
|
|
42
|
+
}
|
|
43
|
+
.qsl\:primary-icon {
|
|
44
|
+
position: absolute;
|
|
45
|
+
display: grid;
|
|
46
|
+
place-items: center;
|
|
47
|
+
opacity: 0;
|
|
48
|
+
transform: scale(0.6) rotate(-30deg);
|
|
49
|
+
transition: opacity 180ms ease, transform 220ms cubic-bezier(0.2, 0.8, 0.2, 1);
|
|
50
|
+
pointer-events: none;
|
|
51
|
+
&[data-active="true"] {
|
|
52
|
+
opacity: 1;
|
|
53
|
+
transform: scale(1) rotate(0);
|
|
54
|
+
}
|
|
55
|
+
& svg {
|
|
56
|
+
width: 22px;
|
|
57
|
+
height: 22px;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
24
60
|
.qsl\:pointer-events-auto {
|
|
25
61
|
pointer-events: auto;
|
|
26
62
|
}
|
|
27
63
|
.qsl\:pointer-events-none {
|
|
28
64
|
pointer-events: none;
|
|
29
65
|
}
|
|
66
|
+
.qsl\:recording-dot {
|
|
67
|
+
position: absolute;
|
|
68
|
+
right: 7px;
|
|
69
|
+
top: 7px;
|
|
70
|
+
width: 6px;
|
|
71
|
+
height: 6px;
|
|
72
|
+
border-radius: 50%;
|
|
73
|
+
background: currentColor;
|
|
74
|
+
}
|
|
30
75
|
.qsl\:sr-only {
|
|
31
76
|
position: absolute;
|
|
32
77
|
width: 1px;
|
|
@@ -38,6 +83,48 @@
|
|
|
38
83
|
white-space: nowrap;
|
|
39
84
|
border-width: 0;
|
|
40
85
|
}
|
|
86
|
+
.qsl\:primary-action {
|
|
87
|
+
position: relative;
|
|
88
|
+
display: grid;
|
|
89
|
+
flex: none;
|
|
90
|
+
place-items: center;
|
|
91
|
+
width: 48px;
|
|
92
|
+
height: 48px;
|
|
93
|
+
border-radius: 50%;
|
|
94
|
+
background: var(--qsl-pill-primary-background, var(--qsl-primary));
|
|
95
|
+
color: var(--qsl-pill-primary-foreground, var(--qsl-primary-foreground));
|
|
96
|
+
box-shadow: var(--qsl-pill-primary-shadow, 0 5px 20px -5px var(--qsl-primary));
|
|
97
|
+
@supports (color: color-mix(in lab, red, red)) {
|
|
98
|
+
box-shadow: var(--qsl-pill-primary-shadow, 0 5px 20px -5px color-mix(in srgb, var(--qsl-primary) 50%, transparent));
|
|
99
|
+
}
|
|
100
|
+
cursor: pointer;
|
|
101
|
+
touch-action: none;
|
|
102
|
+
user-select: none;
|
|
103
|
+
-webkit-touch-callout: none;
|
|
104
|
+
transition: transform 180ms ease, background-color 180ms ease, color 180ms ease, box-shadow 180ms ease;
|
|
105
|
+
&:hover:not(:disabled) {
|
|
106
|
+
filter: brightness(1.08);
|
|
107
|
+
}
|
|
108
|
+
&:active:not(:disabled) {
|
|
109
|
+
transform: scale(0.94);
|
|
110
|
+
}
|
|
111
|
+
&:focus-visible {
|
|
112
|
+
outline: 3px solid var(--qsl-primary);
|
|
113
|
+
outline-offset: 3px;
|
|
114
|
+
}
|
|
115
|
+
&:disabled {
|
|
116
|
+
opacity: 0.45;
|
|
117
|
+
cursor: default;
|
|
118
|
+
}
|
|
119
|
+
&[data-primary-action="recording"] {
|
|
120
|
+
background: var(--qsl-pill-recording-background, var(--qsl-destructive));
|
|
121
|
+
color: var(--qsl-pill-recording-foreground, var(--qsl-background));
|
|
122
|
+
box-shadow: 0 0 0 4px var(--qsl-pill-aura-color, var(--qsl-destructive));
|
|
123
|
+
@supports (color: color-mix(in lab, red, red)) {
|
|
124
|
+
box-shadow: 0 0 0 4px color-mix(in srgb, var(--qsl-pill-aura-color, var(--qsl-destructive)) 22%, transparent);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
41
128
|
.qsl\:absolute {
|
|
42
129
|
position: absolute;
|
|
43
130
|
}
|
|
@@ -131,6 +218,45 @@
|
|
|
131
218
|
.qsl\:ml-1\.5 {
|
|
132
219
|
margin-left: calc(var(--qsl-spacing) * 1.5);
|
|
133
220
|
}
|
|
221
|
+
.qsl\:pill-slot {
|
|
222
|
+
display: flex;
|
|
223
|
+
flex: none;
|
|
224
|
+
align-items: center;
|
|
225
|
+
align-self: flex-end;
|
|
226
|
+
gap: 2px;
|
|
227
|
+
min-height: 48px;
|
|
228
|
+
& > button {
|
|
229
|
+
min-width: 36px;
|
|
230
|
+
min-height: 36px;
|
|
231
|
+
border-radius: 999px;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
.qsl\:pill-replacement {
|
|
235
|
+
display: flex;
|
|
236
|
+
flex: 1;
|
|
237
|
+
align-items: center;
|
|
238
|
+
align-self: center;
|
|
239
|
+
min-width: 0;
|
|
240
|
+
min-height: 48px;
|
|
241
|
+
padding-inline: 8px;
|
|
242
|
+
font-size: var(--qsl-base);
|
|
243
|
+
}
|
|
244
|
+
.qsl\:pill-row {
|
|
245
|
+
display: flex;
|
|
246
|
+
align-items: flex-end;
|
|
247
|
+
gap: 4px;
|
|
248
|
+
min-height: 64px;
|
|
249
|
+
min-width: 0;
|
|
250
|
+
padding: 7px;
|
|
251
|
+
border: 1px solid var(--qsl-pill-border, var(--qsl-border));
|
|
252
|
+
border-radius: 32px;
|
|
253
|
+
background: var(--qsl-pill-background, var(--qsl-background));
|
|
254
|
+
box-shadow: var(--qsl-pill-shadow, var(--qsl-bar-shadow));
|
|
255
|
+
transition: border-color 180ms ease, box-shadow 180ms ease;
|
|
256
|
+
&:focus-within {
|
|
257
|
+
border-color: var(--qsl-primary);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
134
260
|
.qsl\:block {
|
|
135
261
|
display: block;
|
|
136
262
|
}
|
|
@@ -170,6 +296,10 @@
|
|
|
170
296
|
width: calc(var(--qsl-spacing) * 9);
|
|
171
297
|
height: calc(var(--qsl-spacing) * 9);
|
|
172
298
|
}
|
|
299
|
+
.qsl\:size-24 {
|
|
300
|
+
width: calc(var(--qsl-spacing) * 24);
|
|
301
|
+
height: calc(var(--qsl-spacing) * 24);
|
|
302
|
+
}
|
|
173
303
|
.qsl\:h-5 {
|
|
174
304
|
height: calc(var(--qsl-spacing) * 5);
|
|
175
305
|
}
|
|
@@ -209,6 +339,20 @@
|
|
|
209
339
|
.qsl\:max-h-\[min\(70dvh\,620px\,var\(--available-height\)\)\] {
|
|
210
340
|
max-height: min(70dvh, 620px, var(--available-height));
|
|
211
341
|
}
|
|
342
|
+
.qsl\:pill-input {
|
|
343
|
+
flex: 1;
|
|
344
|
+
align-self: center;
|
|
345
|
+
min-width: 0;
|
|
346
|
+
min-height: 48px;
|
|
347
|
+
padding: 12px 8px;
|
|
348
|
+
border-radius: 24px;
|
|
349
|
+
font-size: var(--qsl-base);
|
|
350
|
+
line-height: 1.5;
|
|
351
|
+
&:focus-visible {
|
|
352
|
+
outline: 2px solid var(--qsl-primary);
|
|
353
|
+
outline-offset: -2px;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
212
356
|
.qsl\:min-h-0 {
|
|
213
357
|
min-height: 0px;
|
|
214
358
|
}
|
|
@@ -230,9 +374,6 @@
|
|
|
230
374
|
.qsl\:w-\[190px\] {
|
|
231
375
|
width: 190px;
|
|
232
376
|
}
|
|
233
|
-
.qsl\:w-\[320px\] {
|
|
234
|
-
width: 320px;
|
|
235
|
-
}
|
|
236
377
|
.qsl\:w-\[calc\(100\%_-_2_\*_var\(--qsl-chat-horizontal-padding\,24px\)\)\] {
|
|
237
378
|
width: calc(100% - 2 * var(--qsl-chat-horizontal-padding,24px));
|
|
238
379
|
}
|
|
@@ -565,6 +706,9 @@
|
|
|
565
706
|
.qsl\:object-contain {
|
|
566
707
|
object-fit: contain;
|
|
567
708
|
}
|
|
709
|
+
.qsl\:object-cover {
|
|
710
|
+
object-fit: cover;
|
|
711
|
+
}
|
|
568
712
|
.qsl\:p-0 {
|
|
569
713
|
padding: 0px;
|
|
570
714
|
}
|
|
@@ -1194,6 +1338,11 @@
|
|
|
1194
1338
|
.qsl\:data-\[working\=true\]\:border-primary[data-working="true"] {
|
|
1195
1339
|
border-color: var(--qsl-primary);
|
|
1196
1340
|
}
|
|
1341
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
1342
|
+
.qsl\:motion-safe\:animate-spin {
|
|
1343
|
+
animation: var(--qsl-animate-spin);
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1197
1346
|
@media (prefers-reduced-motion: reduce) {
|
|
1198
1347
|
.qsl\:motion-reduce\:animate-none {
|
|
1199
1348
|
animation: none;
|
|
@@ -1543,6 +1692,16 @@
|
|
|
1543
1692
|
inherits: true;
|
|
1544
1693
|
initial-value: 13px;
|
|
1545
1694
|
}
|
|
1695
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1696
|
+
[data-layout="pill"] *, [data-recording-aura] {
|
|
1697
|
+
transition: none !important;
|
|
1698
|
+
animation: none !important;
|
|
1699
|
+
}
|
|
1700
|
+
[data-recording-aura] {
|
|
1701
|
+
transform: scale(1.3) !important;
|
|
1702
|
+
opacity: 0.6 !important;
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1546
1705
|
@layer components {
|
|
1547
1706
|
[data-quassel-markdown] {
|
|
1548
1707
|
overflow-wrap: anywhere;
|
package/index.d.ts
CHANGED
|
@@ -20,6 +20,6 @@ export { ChatSendContext, createChatSendScope, type ChatSendScope } from "./Chat
|
|
|
20
20
|
export { messageDate, timestampDay, timestampLabel } from "./timestamps";
|
|
21
21
|
export { formatElapsed, useElapsed } from "./elapsed";
|
|
22
22
|
export { attachmentCapabilityError, attachmentDownloadUrl, attachmentMediaType, encodeAttachment, formatAttachmentSize, validateAttachmentSelection, MAX_CHAT_ATTACHMENTS, MAX_CHAT_ATTACHMENT_BYTES, } from "./attachments";
|
|
23
|
-
export type { ChatAppearance, TimestampOptions, CodeBlockOptions, BubbleOptions, MessageAction, MessageActionsOptions, SendShortcut, } from "./options";
|
|
23
|
+
export type { ChatAppearance, TimestampOptions, CodeBlockOptions, BubbleOptions, MessageAction, MessageActionsOptions, SendShortcut, ChatMicrophone, MicrophonePress, } from "./options";
|
|
24
24
|
export { appearanceStyle } from "./options";
|
|
25
25
|
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";
|