@refraction-ui/react 0.16.2 → 0.17.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.
- package/dist/index.cjs +2016 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2000 -12
- package/dist/index.js.map +1 -1
- package/dist/internal/composer/index.d.ts +590 -0
- package/dist/internal/react-composer/index.d.ts +131 -0
- package/package.json +2 -1
|
@@ -0,0 +1,590 @@
|
|
|
1
|
+
import { MessageAttachment } from '../conversation/index.js';
|
|
2
|
+
export { MessageAttachment } from '../conversation/index.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Public types for the headless chat composer core.
|
|
6
|
+
*
|
|
7
|
+
* The composer models a single growing message buffer (flat string + selection)
|
|
8
|
+
* with inline triggers (@mention, /command, :emoji:, #tag, custom), committed
|
|
9
|
+
* atomic tokens, staged attachments, and one optimistic submit. Framework
|
|
10
|
+
* adapters (React/Astro/Flutter) wrap this contract without re-implementing
|
|
11
|
+
* any behavior.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** Where a trigger symbol is allowed to arm. */
|
|
15
|
+
type ComposerTriggerScope = 'anywhere' | 'startOfLine' | 'startOfMessage';
|
|
16
|
+
/** A candidate shown in the suggestion menu for an armed trigger. */
|
|
17
|
+
interface ComposerCandidate {
|
|
18
|
+
id: string;
|
|
19
|
+
display: string;
|
|
20
|
+
subtitle?: string;
|
|
21
|
+
metadata?: Record<string, unknown>;
|
|
22
|
+
}
|
|
23
|
+
type ComposerTriggerResolver = (query: string) => ComposerCandidate[] | Promise<ComposerCandidate[]>;
|
|
24
|
+
/**
|
|
25
|
+
* A trigger is configuration, not code — the engine is symbol-agnostic and
|
|
26
|
+
* never special-cases '@' / '/' / ':' in control flow.
|
|
27
|
+
*/
|
|
28
|
+
interface ComposerTrigger {
|
|
29
|
+
id: string;
|
|
30
|
+
/** Trigger symbol; any length ('@', '/', '#', '!!'). */
|
|
31
|
+
symbol: string;
|
|
32
|
+
/** Default 'anywhere'. Slash-style commands use 'startOfMessage'. */
|
|
33
|
+
scope?: ComposerTriggerScope;
|
|
34
|
+
/** Extra validation for the query; a violating query closes the trigger. */
|
|
35
|
+
queryPattern?: RegExp;
|
|
36
|
+
/** Queries longer than this silently cancel the trigger. Default 40. */
|
|
37
|
+
maxQueryLength?: number;
|
|
38
|
+
/** Default true. '#'-style triggers set false so '#weekend trip' stays armed. */
|
|
39
|
+
closeOnSpace?: boolean;
|
|
40
|
+
/** Default false. Trades away email/URL protection — must be justified. */
|
|
41
|
+
allowMidWord?: boolean;
|
|
42
|
+
/** Additional boundary characters allowed before the symbol (e.g. '(' or '"'). */
|
|
43
|
+
extraBoundaryChars?: string[];
|
|
44
|
+
/** Resolver debounce in ms. Default 0 (sync/local resolvers). */
|
|
45
|
+
debounceMs?: number;
|
|
46
|
+
/** Visible slice size for the adapter's menu. Default 6. */
|
|
47
|
+
maxVisibleResults?: number;
|
|
48
|
+
/** Whether ArrowUp/Down wrap around the ends. Default true. */
|
|
49
|
+
wrapNavigation?: boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Builds the committed token's display text. Defaults to `symbol + display`
|
|
52
|
+
* ('@Jordan Lee'); the emoji recipe returns the unicode itself.
|
|
53
|
+
*/
|
|
54
|
+
toDisplay?: (candidate: ComposerCandidate) => string;
|
|
55
|
+
resolve: ComposerTriggerResolver;
|
|
56
|
+
}
|
|
57
|
+
/** A committed, atomic inline unit (identity + frozen display). */
|
|
58
|
+
interface ComposerToken {
|
|
59
|
+
triggerId: string;
|
|
60
|
+
symbol: string;
|
|
61
|
+
id: string;
|
|
62
|
+
label: string;
|
|
63
|
+
display: string;
|
|
64
|
+
metadata?: Record<string, unknown>;
|
|
65
|
+
}
|
|
66
|
+
/** A token placed in the value, with its live UTF-16 range. */
|
|
67
|
+
interface PlacedToken extends ComposerToken {
|
|
68
|
+
start: number;
|
|
69
|
+
end: number;
|
|
70
|
+
}
|
|
71
|
+
/** Structured output token with derived UTF-16 offsets into plainText. */
|
|
72
|
+
interface ResolvedToken {
|
|
73
|
+
type: string;
|
|
74
|
+
id: string;
|
|
75
|
+
display: string;
|
|
76
|
+
start: number;
|
|
77
|
+
end: number;
|
|
78
|
+
}
|
|
79
|
+
type ComposerAttachmentKind = 'text' | 'image' | 'video' | 'audio' | 'file';
|
|
80
|
+
type ComposerAttachmentStatus = 'pending' | 'uploading' | 'ready' | 'error';
|
|
81
|
+
interface ComposerAttachment {
|
|
82
|
+
id: string;
|
|
83
|
+
kind: ComposerAttachmentKind;
|
|
84
|
+
name: string;
|
|
85
|
+
mimeType?: string;
|
|
86
|
+
sizeBytes?: number;
|
|
87
|
+
previewUrl?: string;
|
|
88
|
+
status: ComposerAttachmentStatus;
|
|
89
|
+
/** 0..1 while uploading. */
|
|
90
|
+
progress?: number;
|
|
91
|
+
errorMessage?: string;
|
|
92
|
+
metadata?: Record<string, unknown>;
|
|
93
|
+
}
|
|
94
|
+
/** Input to `addAttachment` — id/status are core-owned defaults. */
|
|
95
|
+
type ComposerAttachmentDraft = Omit<ComposerAttachment, 'id' | 'status'> & {
|
|
96
|
+
id?: string;
|
|
97
|
+
status?: ComposerAttachmentStatus;
|
|
98
|
+
};
|
|
99
|
+
interface ComposerSubmission {
|
|
100
|
+
/** Trimmed message text with token displays inlined. */
|
|
101
|
+
plainText: string;
|
|
102
|
+
tokens: ResolvedToken[];
|
|
103
|
+
attachments: ComposerAttachment[];
|
|
104
|
+
replyToMessageId?: string;
|
|
105
|
+
/** Present when submitting from edit mode. */
|
|
106
|
+
editingMessageId?: string;
|
|
107
|
+
}
|
|
108
|
+
interface ComposerOutput {
|
|
109
|
+
plainText: string;
|
|
110
|
+
tokens: ResolvedToken[];
|
|
111
|
+
}
|
|
112
|
+
interface ComposerSelection {
|
|
113
|
+
start: number;
|
|
114
|
+
end: number;
|
|
115
|
+
}
|
|
116
|
+
interface ActiveTriggerState {
|
|
117
|
+
triggerId: string;
|
|
118
|
+
symbol: string;
|
|
119
|
+
symbolStart: number;
|
|
120
|
+
caret: number;
|
|
121
|
+
query: string;
|
|
122
|
+
}
|
|
123
|
+
interface SuggestionState {
|
|
124
|
+
isOpen: boolean;
|
|
125
|
+
/** Full result list (overflow intact). */
|
|
126
|
+
items: ComposerCandidate[];
|
|
127
|
+
/** `items` sliced to the trigger's maxVisibleResults for the adapter. */
|
|
128
|
+
visibleItems: ComposerCandidate[];
|
|
129
|
+
activeIndex: number;
|
|
130
|
+
loading: boolean;
|
|
131
|
+
error: string | null;
|
|
132
|
+
/** Monotonic id of the latest resolve request (staleness guard). */
|
|
133
|
+
requestToken: number;
|
|
134
|
+
}
|
|
135
|
+
interface CounterState {
|
|
136
|
+
/** Visible once remaining budget is within 20% of maxLength. */
|
|
137
|
+
visible: boolean;
|
|
138
|
+
/** Remaining grapheme budget; null without a maxLength. */
|
|
139
|
+
remaining: number | null;
|
|
140
|
+
overLimit: boolean;
|
|
141
|
+
}
|
|
142
|
+
type ComposerMode = 'compose' | 'edit';
|
|
143
|
+
interface ComposerState {
|
|
144
|
+
value: string;
|
|
145
|
+
selection: ComposerSelection;
|
|
146
|
+
isComposing: boolean;
|
|
147
|
+
isBusy: boolean;
|
|
148
|
+
disabled: boolean;
|
|
149
|
+
readOnly: boolean;
|
|
150
|
+
isEmpty: boolean;
|
|
151
|
+
canSend: boolean;
|
|
152
|
+
error: string | null;
|
|
153
|
+
attachments: ComposerAttachment[];
|
|
154
|
+
tokens: PlacedToken[];
|
|
155
|
+
activeTrigger: ActiveTriggerState | null;
|
|
156
|
+
suggestion: SuggestionState;
|
|
157
|
+
counter: CounterState;
|
|
158
|
+
mode: ComposerMode;
|
|
159
|
+
editingMessageId?: string;
|
|
160
|
+
}
|
|
161
|
+
interface ComposerValidationResult {
|
|
162
|
+
isValid: boolean;
|
|
163
|
+
reason?: string;
|
|
164
|
+
}
|
|
165
|
+
type ComposerValidator = (plainText: string, tokens: ResolvedToken[]) => ComposerValidationResult;
|
|
166
|
+
/** Persisted draft snapshot (attachments by id only — blobs are host-owned). */
|
|
167
|
+
interface ComposerDraft {
|
|
168
|
+
value: string;
|
|
169
|
+
tokens: PlacedToken[];
|
|
170
|
+
attachmentIds: string[];
|
|
171
|
+
updatedAt: number;
|
|
172
|
+
}
|
|
173
|
+
/** Injected persistence seam; the core ships only an in-memory default. */
|
|
174
|
+
interface ComposerDraftStore {
|
|
175
|
+
read(key: string): ComposerDraft | null;
|
|
176
|
+
write(key: string, draft: ComposerDraft): void;
|
|
177
|
+
clear(key: string): void;
|
|
178
|
+
}
|
|
179
|
+
type ComposerEvent = {
|
|
180
|
+
type: 'paste-trimmed';
|
|
181
|
+
} | {
|
|
182
|
+
type: 'insert-rejected';
|
|
183
|
+
reason: 'max-length' | 'composing' | 'disabled' | 'read-only';
|
|
184
|
+
} | {
|
|
185
|
+
type: 'edit-rejected';
|
|
186
|
+
reason: 'inside-token';
|
|
187
|
+
} | {
|
|
188
|
+
type: 'attachment-rejected';
|
|
189
|
+
reason: 'max-attachments' | 'max-size' | 'not-accepted';
|
|
190
|
+
name: string;
|
|
191
|
+
detail?: string;
|
|
192
|
+
} | {
|
|
193
|
+
type: 'typing';
|
|
194
|
+
};
|
|
195
|
+
interface ComposerConfig {
|
|
196
|
+
initialValue?: string;
|
|
197
|
+
/** Tokens present in initialValue (e.g. when re-opening a draft the host owns). */
|
|
198
|
+
initialTokens?: PlacedToken[];
|
|
199
|
+
/** Grapheme-cluster budget (not UTF-16 units). */
|
|
200
|
+
maxLength?: number;
|
|
201
|
+
maxAttachments?: number;
|
|
202
|
+
/** Per-attachment size gate; larger drafts are rejected with an event. */
|
|
203
|
+
maxAttachmentSizeBytes?: number;
|
|
204
|
+
/** Predicate gate; return false or a reason string to reject a draft. */
|
|
205
|
+
acceptAttachment?: (draft: ComposerAttachmentDraft) => boolean | string;
|
|
206
|
+
minLines?: number;
|
|
207
|
+
maxLines?: number;
|
|
208
|
+
triggers?: ComposerTrigger[];
|
|
209
|
+
validator?: ComposerValidator;
|
|
210
|
+
draftStore?: ComposerDraftStore;
|
|
211
|
+
draftKey?: string;
|
|
212
|
+
/** Draft autosave debounce. Default 400ms. */
|
|
213
|
+
draftDebounceMs?: number;
|
|
214
|
+
/** Leading-edge throttle for the 'typing' event. Default 3000ms. */
|
|
215
|
+
typingSignalIntervalMs?: number;
|
|
216
|
+
replyToMessageId?: string;
|
|
217
|
+
/** Injected clock — the core never calls Date.now() in logic paths. */
|
|
218
|
+
now?: () => number;
|
|
219
|
+
/** Injected id factory — the core never calls Math.random(). */
|
|
220
|
+
generateId?: (prefix?: string) => string;
|
|
221
|
+
/** Notice channel ('paste-trimmed', 'attachment-rejected', 'typing', …). */
|
|
222
|
+
onEvent?: (event: ComposerEvent) => void;
|
|
223
|
+
}
|
|
224
|
+
/** Outcome of a physical Enter press routed through the core. */
|
|
225
|
+
type EnterResult = 'submitted' | 'newline' | 'committed-suggestion' | 'noop';
|
|
226
|
+
interface ComposerAPI {
|
|
227
|
+
getState(): ComposerState;
|
|
228
|
+
subscribe(listener: (state: ComposerState) => void): () => void;
|
|
229
|
+
/**
|
|
230
|
+
* The adapter's input path: full new text + selection after a user edit.
|
|
231
|
+
* Pass `{ programmatic: true }` for host-driven writes (suppresses the
|
|
232
|
+
* typing signal).
|
|
233
|
+
*/
|
|
234
|
+
setValue(text: string, selection?: ComposerSelection, opts?: {
|
|
235
|
+
programmatic?: boolean;
|
|
236
|
+
}): void;
|
|
237
|
+
/** Caret/selection move without a text change (allowed while readOnly). */
|
|
238
|
+
setSelection(selection: ComposerSelection): void;
|
|
239
|
+
insertTextAtCursor(text: string): void;
|
|
240
|
+
setComposing(isComposing: boolean): void;
|
|
241
|
+
/** Routes a physical Enter; the return value tells the adapter whether to preventDefault. */
|
|
242
|
+
applyEnter(opts: {
|
|
243
|
+
shiftPressed: boolean;
|
|
244
|
+
}): EnterResult;
|
|
245
|
+
moveSuggestionNext(): void;
|
|
246
|
+
moveSuggestionPrevious(): void;
|
|
247
|
+
/** Pointer hover — last-input-wins with the keyboard. */
|
|
248
|
+
setSuggestionActiveIndex(index: number): void;
|
|
249
|
+
applySuggestion(index?: number): void;
|
|
250
|
+
/** Escape-style dismissal: closes now and marks the occurrence dismissed. */
|
|
251
|
+
dismissSuggestion(): void;
|
|
252
|
+
/** Blur-style dismissal: closes after a short grace so a click can land. */
|
|
253
|
+
dismissSuggestionDeferred(): void;
|
|
254
|
+
retrySuggestions(): void;
|
|
255
|
+
addAttachment(draft: ComposerAttachmentDraft): string | null;
|
|
256
|
+
updateAttachment(id: string, patch: Partial<Omit<ComposerAttachment, 'id'>>): void;
|
|
257
|
+
removeAttachment(id: string): void;
|
|
258
|
+
setBusy(busy: boolean): void;
|
|
259
|
+
setError(error: string | null): void;
|
|
260
|
+
setDisabled(disabled: boolean): void;
|
|
261
|
+
setReadOnly(readOnly: boolean): void;
|
|
262
|
+
beginEdit(args: {
|
|
263
|
+
value: string;
|
|
264
|
+
tokens?: PlacedToken[];
|
|
265
|
+
messageId: string;
|
|
266
|
+
}): void;
|
|
267
|
+
cancelEdit(): void;
|
|
268
|
+
/** Clipboard seam: returns the selected display text. */
|
|
269
|
+
copySelection(): string;
|
|
270
|
+
/** Clipboard seam: removes the selection, retaining token identity for same-instance paste. */
|
|
271
|
+
cutSelection(): string;
|
|
272
|
+
/** Clipboard seam: plain-text insert at caret (clamped); restores tokens only for a same-instance cut. */
|
|
273
|
+
pasteText(text: string): void;
|
|
274
|
+
submit(): ComposerSubmission | null;
|
|
275
|
+
reset(): void;
|
|
276
|
+
serialize(): ComposerOutput;
|
|
277
|
+
destroy(): void;
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Bridge to the conversation layer: a ready composer attachment mapped onto
|
|
281
|
+
* `@refraction-ui/conversation`'s wire shape.
|
|
282
|
+
*/
|
|
283
|
+
declare function toMessageAttachment(attachment: ComposerAttachment): MessageAttachment;
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Grapheme-cluster utilities.
|
|
287
|
+
*
|
|
288
|
+
* Length limits and clamping must never bisect a user-perceived character —
|
|
289
|
+
* a 👨👩👧👦 ZWJ family emoji is 11 UTF-16 units but one cluster. We use
|
|
290
|
+
* `Intl.Segmenter` when available; the fallback treats each code point as a
|
|
291
|
+
* cluster (coarser, but it never splits a surrogate pair).
|
|
292
|
+
*/
|
|
293
|
+
/** Count of grapheme clusters (code points in the fallback), never UTF-16 units. */
|
|
294
|
+
declare function graphemeLength(text: string): number;
|
|
295
|
+
/**
|
|
296
|
+
* Clamp `text` to at most `max` grapheme clusters. A cluster that would cross
|
|
297
|
+
* the limit is rejected whole — the result is always a valid cluster boundary.
|
|
298
|
+
*/
|
|
299
|
+
declare function clampGraphemes(text: string, max: number): string;
|
|
300
|
+
/** Whether `offset` sits on a grapheme-cluster boundary of `text`. */
|
|
301
|
+
declare function isGraphemeBoundary(text: string, offset: number): boolean;
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Pure composer rules — the single source of truth shared by the enable-check,
|
|
305
|
+
* the Enter handler, and the submitted payload (so they can never disagree).
|
|
306
|
+
*/
|
|
307
|
+
|
|
308
|
+
/** One trim shared by the enable-check AND the sent payload. */
|
|
309
|
+
declare function payloadFor(raw: string): string;
|
|
310
|
+
interface CanSendArgs {
|
|
311
|
+
text: string;
|
|
312
|
+
attachments?: readonly unknown[];
|
|
313
|
+
disabled?: boolean;
|
|
314
|
+
readOnly?: boolean;
|
|
315
|
+
busy?: boolean;
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Send is derived from BOTH trimmed text and attachments — an attachments-only
|
|
319
|
+
* message is valid; a whitespace-only message never is.
|
|
320
|
+
*/
|
|
321
|
+
declare function canSend({ text, attachments, disabled, readOnly, busy }: CanSendArgs): boolean;
|
|
322
|
+
/**
|
|
323
|
+
* The platform-matrix resolver: physical Enter sends unless Shift is held or
|
|
324
|
+
* an IME composition is active (composing always wins — Enter confirms the
|
|
325
|
+
* candidate, never submits).
|
|
326
|
+
*/
|
|
327
|
+
declare function shouldSubmitOnEnter({ shiftPressed, isComposing, }: {
|
|
328
|
+
shiftPressed: boolean;
|
|
329
|
+
isComposing: boolean;
|
|
330
|
+
}): boolean;
|
|
331
|
+
/** The counter surfaces once remaining budget is within this fraction of maxLength. */
|
|
332
|
+
declare const COUNTER_VISIBLE_FRACTION = 0.2;
|
|
333
|
+
/**
|
|
334
|
+
* Counter math over grapheme budget. The counter is confirmatory, not the
|
|
335
|
+
* enforcement mechanism (clamping at the insert boundary prevents overflow).
|
|
336
|
+
*/
|
|
337
|
+
declare function computeCounter(text: string, maxLength: number | undefined): CounterState;
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Symbol-agnostic trigger detection (R10–R12).
|
|
341
|
+
*
|
|
342
|
+
* Detection runs once per mutation, scanning backward from the caret within a
|
|
343
|
+
* bounded window — cost is independent of how much text precedes the trigger.
|
|
344
|
+
* The simple single-char/anywhere case delegates to rich-editor's shared
|
|
345
|
+
* `detectTriggerInText` so the boundary rules live in exactly one place; the
|
|
346
|
+
* general path (multi-char symbols, scopes, extra boundary chars,
|
|
347
|
+
* closeOnSpace=false) layers on top of the same candidate/validation split.
|
|
348
|
+
*/
|
|
349
|
+
|
|
350
|
+
declare const DEFAULT_MAX_QUERY_LENGTH = 40;
|
|
351
|
+
declare const DEFAULT_MAX_VISIBLE_RESULTS = 6;
|
|
352
|
+
/**
|
|
353
|
+
* UTF-16 budget per grapheme when sizing the backward-scan window. The widest
|
|
354
|
+
* common cluster (ZWJ family emoji) is 11 units; 16 leaves headroom without
|
|
355
|
+
* unbounding the scan.
|
|
356
|
+
*/
|
|
357
|
+
declare const SCAN_UNITS_PER_GRAPHEME = 16;
|
|
358
|
+
interface ResolvedTrigger {
|
|
359
|
+
id: string;
|
|
360
|
+
symbol: string;
|
|
361
|
+
scope: ComposerTriggerScope;
|
|
362
|
+
queryPattern: RegExp | null;
|
|
363
|
+
maxQueryLength: number;
|
|
364
|
+
closeOnSpace: boolean;
|
|
365
|
+
allowMidWord: boolean;
|
|
366
|
+
extraBoundaryChars: readonly string[];
|
|
367
|
+
debounceMs: number;
|
|
368
|
+
maxVisibleResults: number;
|
|
369
|
+
wrapNavigation: boolean;
|
|
370
|
+
toDisplay: (candidate: ComposerCandidate) => string;
|
|
371
|
+
resolve: ComposerTrigger['resolve'];
|
|
372
|
+
}
|
|
373
|
+
declare function resolveTriggerConfig(trigger: ComposerTrigger): ResolvedTrigger;
|
|
374
|
+
/** UTF-16 size of the backward-scan window for a trigger (exported for tests). */
|
|
375
|
+
declare function scanWindowFor(trigger: ResolvedTrigger): number;
|
|
376
|
+
interface TriggerMatch {
|
|
377
|
+
trigger: ResolvedTrigger;
|
|
378
|
+
symbolStart: number;
|
|
379
|
+
caret: number;
|
|
380
|
+
query: string;
|
|
381
|
+
}
|
|
382
|
+
/** An occurrence dismissed via Escape — never re-arms until the symbol is retyped. */
|
|
383
|
+
interface DismissedOccurrence {
|
|
384
|
+
triggerId: string;
|
|
385
|
+
symbolStart: number;
|
|
386
|
+
}
|
|
387
|
+
interface TokenRange {
|
|
388
|
+
start: number;
|
|
389
|
+
end: number;
|
|
390
|
+
}
|
|
391
|
+
interface DetectTriggerArgs {
|
|
392
|
+
text: string;
|
|
393
|
+
caret: number;
|
|
394
|
+
triggers: readonly ResolvedTrigger[];
|
|
395
|
+
isComposing?: boolean;
|
|
396
|
+
dismissed?: readonly DismissedOccurrence[];
|
|
397
|
+
tokenRanges?: readonly TokenRange[];
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Backward-scan detection from the caret. Returns the nearest live trigger
|
|
401
|
+
* (only the last unescaped occurrence before the caret can be live), or null.
|
|
402
|
+
* Suspended entirely while an IME composition is active.
|
|
403
|
+
*/
|
|
404
|
+
declare function detectActiveTrigger({ text, caret, triggers, isComposing, dismissed, tokenRanges, }: DetectTriggerArgs): TriggerMatch | null;
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Suggestion menu state (R13, R14): debounced async resolution with a
|
|
408
|
+
* monotonic requestToken staleness guard, delayed loading state, empty/error
|
|
409
|
+
* states with retry, a per-trigger circuit breaker, and keyboard navigation.
|
|
410
|
+
*
|
|
411
|
+
* Timers are plain setTimeout/clearTimeout — cancelable and deterministic
|
|
412
|
+
* under vitest fake timers; the controller never touches DOM globals.
|
|
413
|
+
*/
|
|
414
|
+
|
|
415
|
+
/** Loading UI appears only if the resolver is still pending after this long. */
|
|
416
|
+
declare const LOADING_DELAY_MS = 120;
|
|
417
|
+
/** Deferred (blur) dismissal grace so a pointer tap on a row can land first. */
|
|
418
|
+
declare const DISMISS_GRACE_MS = 120;
|
|
419
|
+
/** Consecutive resolver throws for one trigger before remote calls stop for the session. */
|
|
420
|
+
declare const CIRCUIT_BREAKER_THRESHOLD = 2;
|
|
421
|
+
declare const CLOSED_SUGGESTION_STATE: SuggestionState;
|
|
422
|
+
interface SuggestionController {
|
|
423
|
+
readonly state: SuggestionState;
|
|
424
|
+
/** Reconcile with the current trigger match after any mutation/caret move. */
|
|
425
|
+
sync(match: TriggerMatch | null): void;
|
|
426
|
+
moveNext(): void;
|
|
427
|
+
movePrevious(): void;
|
|
428
|
+
setActiveIndex(index: number): void;
|
|
429
|
+
retry(): void;
|
|
430
|
+
/** Close after a grace period (blur); a commit during the grace cancels it. */
|
|
431
|
+
dismissDeferred(onClosed: () => void): void;
|
|
432
|
+
cancelDeferredDismiss(): void;
|
|
433
|
+
/** Close immediately and invalidate any in-flight resolution. */
|
|
434
|
+
close(): void;
|
|
435
|
+
isTripped(triggerId: string): boolean;
|
|
436
|
+
destroy(): void;
|
|
437
|
+
}
|
|
438
|
+
declare function createSuggestionController({ emit }: {
|
|
439
|
+
emit: () => void;
|
|
440
|
+
}): SuggestionController;
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Token bookkeeping over the flat string (R15–R18).
|
|
444
|
+
*
|
|
445
|
+
* Committed tokens are atomic: an edit touching part of a token removes the
|
|
446
|
+
* whole token; an insertion strictly inside a token is rejected; the caret
|
|
447
|
+
* never rests strictly inside a token. Every function here is pure — the
|
|
448
|
+
* composer owns the state and feeds it through these rules on each mutation.
|
|
449
|
+
*/
|
|
450
|
+
|
|
451
|
+
interface TextEdit {
|
|
452
|
+
/** Offset where old and new text diverge. */
|
|
453
|
+
start: number;
|
|
454
|
+
/** End of the replaced range in the OLD text. */
|
|
455
|
+
oldEnd: number;
|
|
456
|
+
/** End of the inserted range in the NEW text. */
|
|
457
|
+
newEnd: number;
|
|
458
|
+
}
|
|
459
|
+
/** Minimal single-span diff via common prefix/suffix. Null when texts are equal. */
|
|
460
|
+
declare function diffEdit(oldText: string, newText: string): TextEdit | null;
|
|
461
|
+
interface AppliedEdit {
|
|
462
|
+
value: string;
|
|
463
|
+
tokens: PlacedToken[];
|
|
464
|
+
selection: ComposerSelection;
|
|
465
|
+
/** Set when an insertion strictly inside a token was rejected (value restored). */
|
|
466
|
+
rejected: boolean;
|
|
467
|
+
/** True when maxLength clamping dropped part of the inserted text. */
|
|
468
|
+
trimmed: boolean;
|
|
469
|
+
removedTokens: PlacedToken[];
|
|
470
|
+
/** The edit as actually applied to the old value (post token expansion), or null. */
|
|
471
|
+
edit: TextEdit | null;
|
|
472
|
+
}
|
|
473
|
+
interface ApplyValueEditArgs {
|
|
474
|
+
oldValue: string;
|
|
475
|
+
newValue: string;
|
|
476
|
+
tokens: readonly PlacedToken[];
|
|
477
|
+
newSelection: ComposerSelection;
|
|
478
|
+
/** Grapheme budget; the INSERTED slice is clamped to fit, never bisecting a cluster. */
|
|
479
|
+
maxLength?: number;
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* Reconcile a raw text edit (as reported by the adapter's input) with the
|
|
483
|
+
* committed token list. Deletions touching part of a token expand to the whole
|
|
484
|
+
* token; insertions strictly inside a token are rejected outright.
|
|
485
|
+
*/
|
|
486
|
+
declare function applyValueEdit({ oldValue, newValue, tokens, newSelection, maxLength, }: ApplyValueEditArgs): AppliedEdit;
|
|
487
|
+
/**
|
|
488
|
+
* Snap a selection so no endpoint rests strictly inside a token. A collapsed
|
|
489
|
+
* caret snaps in the direction of travel (so arrow keys skip a token as one
|
|
490
|
+
* unit); a range expands outward to the full token bounds.
|
|
491
|
+
*/
|
|
492
|
+
declare function snapSelectionToTokens(tokens: readonly PlacedToken[], selection: ComposerSelection, previous?: ComposerSelection): ComposerSelection;
|
|
493
|
+
/** Expand a range outward so it covers any partially-included token whole. */
|
|
494
|
+
declare function expandRangeOverTokens(tokens: readonly PlacedToken[], range: ComposerSelection): ComposerSelection;
|
|
495
|
+
interface CommitTokenArgs {
|
|
496
|
+
value: string;
|
|
497
|
+
tokens: readonly PlacedToken[];
|
|
498
|
+
/** Replaced range: `[start, end)` — symbol + query for a trigger commit. */
|
|
499
|
+
start: number;
|
|
500
|
+
end: number;
|
|
501
|
+
token: ComposerToken;
|
|
502
|
+
}
|
|
503
|
+
interface CommitTokenResult {
|
|
504
|
+
value: string;
|
|
505
|
+
tokens: PlacedToken[];
|
|
506
|
+
selection: ComposerSelection;
|
|
507
|
+
}
|
|
508
|
+
/** Replace `[start, end)` with the token's display in one atomic transaction. */
|
|
509
|
+
declare function commitTokenAt({ value, tokens, start, end, token }: CommitTokenArgs): CommitTokenResult;
|
|
510
|
+
/**
|
|
511
|
+
* Structured output: plainText inlines every display; ranges are derived and
|
|
512
|
+
* guaranteed in sync (`plainText.substring(start, end) === display`).
|
|
513
|
+
*/
|
|
514
|
+
declare function serializeTokens(value: string, tokens: readonly PlacedToken[]): ComposerOutput;
|
|
515
|
+
interface TypedEmojiHit {
|
|
516
|
+
shortcode: string;
|
|
517
|
+
unicode: string;
|
|
518
|
+
/** Offset of the opening colon. */
|
|
519
|
+
start: number;
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Detect a just-completed `:shortcode:` immediately before the caret using
|
|
523
|
+
* rich-editor's EMOJI_MAP (via detectEmojiShortcode). Unknown shortcodes stay
|
|
524
|
+
* literal; the composer converts a hit into a committed token without the
|
|
525
|
+
* menu ever opening.
|
|
526
|
+
*/
|
|
527
|
+
declare function detectTypedEmoji(value: string, caret: number): TypedEmojiHit | null;
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* `createComposer(config)` — the headless composer core.
|
|
531
|
+
*
|
|
532
|
+
* Owns the flat value + selection, committed tokens, staged attachments,
|
|
533
|
+
* trigger/suggestion lifecycle, drafts, and the optimistic submit. Pure of
|
|
534
|
+
* DOM/browser globals: `now`/`generateId` are injected (defaults live only at
|
|
535
|
+
* this boundary, matching the other cores in the repo), so the same instance
|
|
536
|
+
* is hydration-deterministic and portable to any framework adapter.
|
|
537
|
+
*/
|
|
538
|
+
|
|
539
|
+
declare const DEFAULT_DRAFT_DEBOUNCE_MS = 400;
|
|
540
|
+
declare const DEFAULT_TYPING_SIGNAL_INTERVAL_MS = 3000;
|
|
541
|
+
declare const DEFAULT_MIN_LINES = 1;
|
|
542
|
+
declare const DEFAULT_MAX_LINES = 6;
|
|
543
|
+
declare function createComposer(config?: ComposerConfig): ComposerAPI;
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* cva style variants for the composer surface, pill, tray, and menu.
|
|
547
|
+
* Boolean-ish variants use string 'true'/'false' keys per repo convention
|
|
548
|
+
* (raw booleans fail the dts build).
|
|
549
|
+
*/
|
|
550
|
+
/** Outer surface (the rounded card wrapping tray + field + action row). */
|
|
551
|
+
declare const composerSurfaceVariants: (props?: ({
|
|
552
|
+
disabled?: "true" | "false" | undefined;
|
|
553
|
+
error?: "true" | "false" | undefined;
|
|
554
|
+
} & {
|
|
555
|
+
className?: string;
|
|
556
|
+
}) | undefined) => string;
|
|
557
|
+
/** The text field itself (borderless inside the surface). */
|
|
558
|
+
declare const composerFieldClass = "block w-full resize-none bg-transparent px-3.5 py-3 text-sm placeholder:text-muted-foreground focus:outline-none";
|
|
559
|
+
/** Attachment tray above the field, inside the dock. */
|
|
560
|
+
declare const composerTrayClass = "flex flex-wrap gap-2 px-3 pt-3";
|
|
561
|
+
/** A single attachment chip. */
|
|
562
|
+
declare const composerAttachmentChipVariants: (props?: ({
|
|
563
|
+
status?: "pending" | "uploading" | "ready" | "error" | undefined;
|
|
564
|
+
} & {
|
|
565
|
+
className?: string;
|
|
566
|
+
}) | undefined) => string;
|
|
567
|
+
/** A committed inline token pill rendered by adapters over the flat string. */
|
|
568
|
+
declare const composerTokenPillClass = "rounded bg-accent/40 px-0.5 text-accent-foreground";
|
|
569
|
+
/** The caret-anchored suggestion menu. */
|
|
570
|
+
declare const composerMenuClass = "z-20 w-72 overflow-hidden rounded-xl border border-border bg-popover shadow-lg";
|
|
571
|
+
/** A suggestion row; `active` follows keyboard/pointer last-input-wins. */
|
|
572
|
+
declare const composerMenuItemVariants: (props?: ({
|
|
573
|
+
active?: "true" | "false" | undefined;
|
|
574
|
+
} & {
|
|
575
|
+
className?: string;
|
|
576
|
+
}) | undefined) => string;
|
|
577
|
+
/** Character counter; switches to the attention treatment at/over the limit. */
|
|
578
|
+
declare const composerCounterVariants: (props?: ({
|
|
579
|
+
overLimit?: "true" | "false" | undefined;
|
|
580
|
+
} & {
|
|
581
|
+
className?: string;
|
|
582
|
+
}) | undefined) => string;
|
|
583
|
+
/** Primary action (send ⇄ stop swap driven by `{hasText, canSend, isBusy}`). */
|
|
584
|
+
declare const composerPrimaryActionVariants: (props?: ({
|
|
585
|
+
enabled?: "true" | "false" | undefined;
|
|
586
|
+
} & {
|
|
587
|
+
className?: string;
|
|
588
|
+
}) | undefined) => string;
|
|
589
|
+
|
|
590
|
+
export { type ActiveTriggerState, type AppliedEdit, type ApplyValueEditArgs, CIRCUIT_BREAKER_THRESHOLD, CLOSED_SUGGESTION_STATE, COUNTER_VISIBLE_FRACTION, type CanSendArgs, type CommitTokenArgs, type CommitTokenResult, type ComposerAPI, type ComposerAttachment, type ComposerAttachmentDraft, type ComposerAttachmentKind, type ComposerAttachmentStatus, type ComposerCandidate, type ComposerConfig, type ComposerDraft, type ComposerDraftStore, type ComposerEvent, type ComposerMode, type ComposerOutput, type ComposerSelection, type ComposerState, type ComposerSubmission, type ComposerToken, type ComposerTrigger, type ComposerTriggerResolver, type ComposerTriggerScope, type ComposerValidationResult, type ComposerValidator, type CounterState, DEFAULT_DRAFT_DEBOUNCE_MS, DEFAULT_MAX_LINES, DEFAULT_MAX_QUERY_LENGTH, DEFAULT_MAX_VISIBLE_RESULTS, DEFAULT_MIN_LINES, DEFAULT_TYPING_SIGNAL_INTERVAL_MS, DISMISS_GRACE_MS, type DetectTriggerArgs, type DismissedOccurrence, type EnterResult, LOADING_DELAY_MS, type PlacedToken, type ResolvedToken, type ResolvedTrigger, SCAN_UNITS_PER_GRAPHEME, type SuggestionController, type SuggestionState, type TextEdit, type TokenRange, type TriggerMatch, type TypedEmojiHit, applyValueEdit, canSend, clampGraphemes, commitTokenAt, composerAttachmentChipVariants, composerCounterVariants, composerFieldClass, composerMenuClass, composerMenuItemVariants, composerPrimaryActionVariants, composerSurfaceVariants, composerTokenPillClass, composerTrayClass, computeCounter, createComposer, createSuggestionController, detectActiveTrigger, detectTypedEmoji, diffEdit, expandRangeOverTokens, graphemeLength, isGraphemeBoundary, payloadFor, resolveTriggerConfig, scanWindowFor, serializeTokens, shouldSubmitOnEnter, snapSelectionToTokens, toMessageAttachment };
|