bsky-richtext-react 1.0.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,761 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import * as react from 'react';
3
+ import { HTMLAttributes, ReactNode, AnchorHTMLAttributes, Ref } from 'react';
4
+ import { SuggestionOptions, SuggestionProps, SuggestionKeyDownProps } from '@tiptap/suggestion';
5
+
6
+ /**
7
+ * TypeScript types mirroring the `app.bsky.richtext.facet` lexicon.
8
+ *
9
+ * Spec: /lexicons/app/richtext/facet.json
10
+ * Reference: https://atproto.com/lexicons/app-bsky-richtext
11
+ */
12
+ /**
13
+ * Specifies the sub-string range a facet feature applies to.
14
+ * Indices are ZERO-indexed byte offsets of the UTF-8 encoded text.
15
+ * - byteStart: inclusive
16
+ * - byteEnd: exclusive
17
+ *
18
+ * ⚠️ JavaScript strings are UTF-16. Always convert to a UTF-8 byte array
19
+ * before computing byte offsets.
20
+ */
21
+ interface ByteSlice {
22
+ byteStart: number;
23
+ byteEnd: number;
24
+ }
25
+ /** Mention of another AT Protocol account. The DID is the canonical identifier. */
26
+ interface MentionFeature {
27
+ $type: 'app.bsky.richtext.facet#mention';
28
+ /** DID of the mentioned account */
29
+ did: string;
30
+ }
31
+ /** A hyperlink facet. The URI is the full, unshortened URL. */
32
+ interface LinkFeature {
33
+ $type: 'app.bsky.richtext.facet#link';
34
+ /** The full URL */
35
+ uri: string;
36
+ }
37
+ /** A hashtag facet. The tag value does NOT include the leading '#'. */
38
+ interface TagFeature {
39
+ $type: 'app.bsky.richtext.facet#tag';
40
+ /** The tag text without the '#' prefix (max 64 graphemes / 640 bytes) */
41
+ tag: string;
42
+ }
43
+ /** Union of all possible facet feature types. */
44
+ type FacetFeature = MentionFeature | LinkFeature | TagFeature;
45
+ /**
46
+ * A single richtext annotation — maps a byte-range within the post text
47
+ * to one or more semantic features (mention, link, tag).
48
+ */
49
+ interface Facet {
50
+ index: ByteSlice;
51
+ features: FacetFeature[];
52
+ }
53
+ /**
54
+ * Represents the `text` + `facets` fields as they appear in an AT Protocol
55
+ * record (e.g. `app.bsky.feed.post`).
56
+ */
57
+ interface RichTextRecord {
58
+ text: string;
59
+ facets?: Facet[];
60
+ }
61
+ /**
62
+ * A parsed segment of richtext — a slice of text with its associated feature
63
+ * (if any). Produced by the richtext parser.
64
+ */
65
+ interface RichTextSegment {
66
+ /** The raw text of this segment */
67
+ text: string;
68
+ /** The facet feature associated with this segment, if any */
69
+ feature?: FacetFeature;
70
+ }
71
+ declare function isMentionFeature(feature: FacetFeature): feature is MentionFeature;
72
+ declare function isLinkFeature(feature: FacetFeature): feature is LinkFeature;
73
+ declare function isTagFeature(feature: FacetFeature): feature is TagFeature;
74
+
75
+ /**
76
+ * ClassNames type definitions for bsky-richtext-react components.
77
+ *
78
+ * Each interface describes the styleable parts of a component as an optional
79
+ * nested object of CSS class strings. Use `generateClassNames()` to deep-merge
80
+ * multiple classNames objects together (e.g. defaults + your overrides).
81
+ *
82
+ * @example
83
+ * ```tsx
84
+ * import {
85
+ * generateClassNames,
86
+ * defaultEditorClassNames,
87
+ * } from 'bsky-richtext-react'
88
+ *
89
+ * <RichTextEditor
90
+ * classNames={generateClassNames([
91
+ * defaultEditorClassNames,
92
+ * { root: 'border rounded-lg p-2', mention: 'text-blue-500' },
93
+ * ], cn)}
94
+ * />
95
+ * ```
96
+ */
97
+ /**
98
+ * Styleable parts of the `RichTextDisplay` component.
99
+ */
100
+ interface DisplayClassNames {
101
+ /** Root `<span>` wrapper around all richtext content */
102
+ root?: string;
103
+ /** Anchor element wrapping each @mention */
104
+ mention?: string;
105
+ /** Anchor element wrapping each link */
106
+ link?: string;
107
+ /** Anchor element wrapping each #hashtag */
108
+ tag?: string;
109
+ }
110
+ /**
111
+ * Styleable parts of the `MentionSuggestionList` dropdown component.
112
+ * Also nested as `suggestion` inside `EditorClassNames`.
113
+ */
114
+ interface SuggestionClassNames {
115
+ /** Outermost container div */
116
+ root?: string;
117
+ /** Each suggestion row (`<button>`) */
118
+ item?: string;
119
+ /**
120
+ * Class added to the currently highlighted suggestion row.
121
+ * Applied in addition to `item` — not instead of it.
122
+ */
123
+ itemSelected?: string;
124
+ /** Avatar wrapper `<span>` */
125
+ avatar?: string;
126
+ /** Avatar `<img>` element */
127
+ avatarImg?: string;
128
+ /** Initial-letter fallback shown when no avatar URL is available */
129
+ avatarPlaceholder?: string;
130
+ /** Text column container (holds display name + handle) */
131
+ text?: string;
132
+ /** Display name `<span>` */
133
+ name?: string;
134
+ /** `@handle` `<span>` */
135
+ handle?: string;
136
+ /** "No results" empty-state message */
137
+ empty?: string;
138
+ }
139
+ /**
140
+ * Styleable parts of the `RichTextEditor` component.
141
+ */
142
+ interface EditorClassNames {
143
+ /** Root wrapper `<div>` around the editor */
144
+ root?: string;
145
+ /** Inner ProseMirror editable `<div>` (via `.ProseMirror`) */
146
+ content?: string;
147
+ /** Placeholder text element */
148
+ placeholder?: string;
149
+ /** Mention chips rendered inside the editor */
150
+ mention?: string;
151
+ /** Autolink decoration spans rendered inside the editor */
152
+ link?: string;
153
+ /** Class names forwarded to the nested `MentionSuggestionList` */
154
+ suggestion?: SuggestionClassNames;
155
+ }
156
+
157
+ interface MentionProps {
158
+ /** The raw segment text (e.g. "@alice.bsky.social") */
159
+ text: string;
160
+ /** The resolved DID of the mentioned account */
161
+ did: string;
162
+ /** The mention facet feature */
163
+ feature: MentionFeature;
164
+ }
165
+ interface LinkProps {
166
+ /** The display text for the link (may be shortened) */
167
+ text: string;
168
+ /** The full URL */
169
+ uri: string;
170
+ /** The link facet feature */
171
+ feature: LinkFeature;
172
+ }
173
+ interface TagProps {
174
+ /** The display text including '#' (e.g. "#atproto") */
175
+ text: string;
176
+ /** The tag value without '#' prefix (e.g. "atproto") */
177
+ tag: string;
178
+ /** The tag facet feature */
179
+ feature: TagFeature;
180
+ }
181
+ interface RichTextDisplayProps extends Omit<HTMLAttributes<HTMLSpanElement>, 'children'> {
182
+ /**
183
+ * The richtext record to render.
184
+ * Accepts `{ text, facets? }` — i.e. the raw AT Protocol record fields.
185
+ */
186
+ value: RichTextRecord | string;
187
+ /**
188
+ * Custom renderer for @mention segments.
189
+ * If not provided, renders a plain `<a>` linking to the profile.
190
+ */
191
+ renderMention?: (props: MentionProps) => ReactNode;
192
+ /**
193
+ * Custom renderer for link segments.
194
+ * If not provided, renders a plain `<a>` with the shortened URL as text.
195
+ */
196
+ renderLink?: (props: LinkProps) => ReactNode;
197
+ /**
198
+ * Custom renderer for #hashtag segments.
199
+ * If not provided, renders a plain `<a>` linking to the hashtag search.
200
+ */
201
+ renderTag?: (props: TagProps) => ReactNode;
202
+ /**
203
+ * When true, all interactive facets (mentions, links, tags) are rendered
204
+ * as plain text with no anchor elements.
205
+ * @default false
206
+ */
207
+ disableLinks?: boolean;
208
+ /**
209
+ * Props forwarded to every `<a>` element rendered by the default renderers.
210
+ * Ignored when custom `renderMention` / `renderLink` / `renderTag` are used.
211
+ */
212
+ linkProps?: AnchorHTMLAttributes<HTMLAnchorElement>;
213
+ /**
214
+ * CSS class names for each styleable part of the component.
215
+ *
216
+ * Use `generateClassNames()` to cleanly merge with the built-in defaults:
217
+ * @example
218
+ * ```tsx
219
+ * import { generateClassNames, defaultDisplayClassNames } from 'bsky-richtext-react'
220
+ *
221
+ * <RichTextDisplay
222
+ * classNames={generateClassNames([
223
+ * defaultDisplayClassNames,
224
+ * { mention: 'text-blue-500 hover:underline' },
225
+ * ], cn)}
226
+ * />
227
+ * ```
228
+ *
229
+ * Or pass a completely custom object to opt out of the defaults entirely:
230
+ * ```tsx
231
+ * <RichTextDisplay classNames={{ root: 'my-richtext', mention: 'my-mention' }} />
232
+ * ```
233
+ */
234
+ classNames?: Partial<DisplayClassNames>;
235
+ /**
236
+ * Generate the `href` for @mention anchors.
237
+ * Called with the mention's DID.
238
+ * @default (did) => `https://bsky.app/profile/${did}`
239
+ *
240
+ * @example Route mentions to your own profile pages
241
+ * ```tsx
242
+ * mentionUrl={(did) => `/profile/${did}`}
243
+ * ```
244
+ */
245
+ mentionUrl?: (did: string) => string;
246
+ /**
247
+ * Generate the `href` for #hashtag anchors.
248
+ * Called with the tag value (without the '#' prefix).
249
+ * @default (tag) => `https://bsky.app/hashtag/${encodeURIComponent(tag)}`
250
+ *
251
+ * @example Route hashtags to your own search page
252
+ * ```tsx
253
+ * tagUrl={(tag) => `/search?tag=${encodeURIComponent(tag)}`}
254
+ * ```
255
+ */
256
+ tagUrl?: (tag: string) => string;
257
+ /**
258
+ * Transform a link URI before it is used as the anchor's `href`.
259
+ * Useful for proxying external links or adding UTM parameters.
260
+ * @default (uri) => uri (identity — no transformation)
261
+ *
262
+ * @example Add a referral parameter
263
+ * ```tsx
264
+ * linkUrl={(uri) => `${uri}?ref=myapp`}
265
+ * ```
266
+ */
267
+ linkUrl?: (uri: string) => string;
268
+ }
269
+ /**
270
+ * `RichTextDisplay` renders AT Protocol richtext content — a string with
271
+ * optional `facets` that annotate byte ranges as mentions, links, or hashtags.
272
+ *
273
+ * @example Basic usage
274
+ * ```tsx
275
+ * <RichTextDisplay value={{ text: post.text, facets: post.facets }} />
276
+ * ```
277
+ *
278
+ * @example With custom mention renderer
279
+ * ```tsx
280
+ * <RichTextDisplay
281
+ * value={post}
282
+ * renderMention={({ text, did }) => (
283
+ * <Link to={`/profile/${did}`}>{text}</Link>
284
+ * )}
285
+ * />
286
+ * ```
287
+ *
288
+ * @example With URL resolvers pointing to your own routes
289
+ * ```tsx
290
+ * <RichTextDisplay
291
+ * value={post}
292
+ * mentionUrl={(did) => `/profile/${did}`}
293
+ * tagUrl={(tag) => `/search?tag=${tag}`}
294
+ * />
295
+ * ```
296
+ *
297
+ * @example With classNames (using generateClassNames for clean merging)
298
+ * ```tsx
299
+ * import { generateClassNames, defaultDisplayClassNames } from 'bsky-richtext-react'
300
+ *
301
+ * <RichTextDisplay
302
+ * value={post}
303
+ * classNames={generateClassNames([
304
+ * defaultDisplayClassNames,
305
+ * { mention: 'text-blue-500 font-semibold' },
306
+ * ], cn)}
307
+ * />
308
+ * ```
309
+ */
310
+ declare function RichTextDisplay({ value, renderMention, renderLink, renderTag, disableLinks, linkProps, classNames: classNamesProp, mentionUrl, tagUrl, linkUrl, ...spanProps }: RichTextDisplayProps): react_jsx_runtime.JSX.Element;
311
+
312
+ /**
313
+ * createSuggestionRenderer
314
+ *
315
+ * Factory that returns a TipTap `SuggestionOptions['render']` function.
316
+ * It uses `tippy.js` for cursor-anchored positioning and `ReactRenderer`
317
+ * to mount the `MentionSuggestionList` React component into the popup.
318
+ *
319
+ * Heavily inspired by Bluesky's social-app Autocomplete.tsx and the
320
+ * official TipTap mention example:
321
+ * https://tiptap.dev/docs/editor/extensions/nodes/mention#usage
322
+ *
323
+ * This is the default renderer used when the consumer does NOT supply
324
+ * a custom `renderMentionSuggestion` prop to `<RichTextEditor>`.
325
+ */
326
+
327
+ interface DefaultSuggestionRendererOptions {
328
+ /**
329
+ * Whether to show avatars in the suggestion list.
330
+ * Forwarded to `MentionSuggestionList`.
331
+ * @default true
332
+ */
333
+ showAvatars?: boolean;
334
+ /**
335
+ * Text shown when the query returns no results.
336
+ * @default "No results"
337
+ */
338
+ noResultsText?: string;
339
+ /**
340
+ * CSS class names for each styleable part of the suggestion dropdown.
341
+ * Forwarded directly to `MentionSuggestionList`.
342
+ */
343
+ classNames?: Partial<SuggestionClassNames>;
344
+ }
345
+ /**
346
+ * Create the default TipTap `suggestion.render` factory.
347
+ *
348
+ * The returned function is called once per "suggestion session"
349
+ * (i.e. each time the user types "@" and a popup should open/update/close).
350
+ *
351
+ * It follows the same lifecycle pattern as the Bluesky reference:
352
+ * - `onStart` → Mount ReactRenderer, create tippy popup
353
+ * - `onUpdate` → Update props, reposition popup
354
+ * - `onKeyDown`→ Delegate to the MentionSuggestionList imperative ref
355
+ * - `onExit` → Destroy popup and React renderer
356
+ */
357
+ declare function createDefaultSuggestionRenderer(options?: DefaultSuggestionRendererOptions): SuggestionOptions<MentionSuggestion>['render'];
358
+
359
+ /**
360
+ * A single suggestion item for the @mention autocomplete popup.
361
+ */
362
+ interface MentionSuggestion {
363
+ /** DID of the suggested account — used as the facet's `did` value */
364
+ did: string;
365
+ /** Display handle (e.g. "alice.bsky.social") */
366
+ handle: string;
367
+ /** Optional display name */
368
+ displayName?: string;
369
+ /** Optional avatar URL */
370
+ avatarUrl?: string;
371
+ }
372
+ /**
373
+ * Imperative ref API for `RichTextEditor`.
374
+ */
375
+ interface RichTextEditorRef {
376
+ /** Focus the editor */
377
+ focus: () => void;
378
+ /** Blur the editor */
379
+ blur: () => void;
380
+ /** Clear the editor content */
381
+ clear: () => void;
382
+ /** Get the current plain-text content */
383
+ getText: () => string;
384
+ }
385
+ interface RichTextEditorProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onChange'> {
386
+ /**
387
+ * Initial richtext value. The editor is pre-populated with this content on mount.
388
+ * This is an uncontrolled initial state — use `onChange` to track updates.
389
+ */
390
+ initialValue?: RichTextRecord | string;
391
+ /**
392
+ * Called on every content change with the latest `RichTextRecord`.
393
+ *
394
+ * The `facets` array is populated via `detectFacetsWithoutResolution()` —
395
+ * facets will contain handles (not DIDs) for mentions until you resolve them
396
+ * server-side using the AT Protocol agent.
397
+ */
398
+ onChange?: (record: RichTextRecord) => void;
399
+ /**
400
+ * Placeholder text shown when the editor is empty.
401
+ */
402
+ placeholder?: string;
403
+ /**
404
+ * Called when the editor gains focus.
405
+ */
406
+ onFocus?: () => void;
407
+ /**
408
+ * Called when the editor loses focus.
409
+ */
410
+ onBlur?: () => void;
411
+ /**
412
+ * Async function to fetch @mention suggestions.
413
+ * Called with the query string (text after "@") as the user types.
414
+ * Return an empty array to show no suggestions / "No results".
415
+ *
416
+ * When not provided, the built-in Bluesky public API search is used
417
+ * (debounced by `mentionSearchDebounceMs`). Set `disableDefaultMentionSearch`
418
+ * to true to disable this default behaviour entirely.
419
+ *
420
+ * @example
421
+ * ```tsx
422
+ * onMentionQuery={async (q) => {
423
+ * const res = await agent.searchActors({ term: q, limit: 8 })
424
+ * return res.data.actors.map(a => ({
425
+ * did: a.did,
426
+ * handle: a.handle,
427
+ * displayName: a.displayName,
428
+ * avatarUrl: a.avatar,
429
+ * }))
430
+ * }}
431
+ * ```
432
+ */
433
+ onMentionQuery?: (query: string) => Promise<MentionSuggestion[]>;
434
+ /**
435
+ * Debounce delay (in milliseconds) applied to the built-in Bluesky mention
436
+ * search. Has no effect when `onMentionQuery` is provided.
437
+ * @default 300
438
+ */
439
+ mentionSearchDebounceMs?: number;
440
+ /**
441
+ * When true, disables the default Bluesky public API mention search.
442
+ * No suggestions will appear unless you provide `onMentionQuery`.
443
+ * @default false
444
+ */
445
+ disableDefaultMentionSearch?: boolean;
446
+ /**
447
+ * Custom TipTap `suggestion.render` factory.
448
+ * When provided, replaces the default tippy.js + MentionSuggestionList renderer.
449
+ * The factory must return `{ onStart, onUpdate, onKeyDown, onExit }`.
450
+ *
451
+ * See: https://tiptap.dev/docs/editor/extensions/nodes/mention#usage
452
+ */
453
+ renderMentionSuggestion?: SuggestionOptions['render'];
454
+ /**
455
+ * Options forwarded to the default suggestion renderer.
456
+ * Only used when `renderMentionSuggestion` is NOT provided.
457
+ */
458
+ mentionSuggestionOptions?: DefaultSuggestionRendererOptions;
459
+ /**
460
+ * CSS class names for each styleable part of the editor.
461
+ *
462
+ * Use `generateClassNames()` to cleanly merge with the built-in defaults:
463
+ * @example
464
+ * ```tsx
465
+ * import { generateClassNames, defaultEditorClassNames } from 'bsky-richtext-react'
466
+ *
467
+ * <RichTextEditor
468
+ * classNames={generateClassNames([
469
+ * defaultEditorClassNames,
470
+ * { root: 'border rounded-lg p-3', mention: 'text-blue-500' },
471
+ * ], cn)}
472
+ * />
473
+ * ```
474
+ *
475
+ * Or pass a completely custom object to opt out of the defaults:
476
+ * ```tsx
477
+ * <RichTextEditor classNames={{ root: 'my-editor', mention: 'my-mention' }} />
478
+ * ```
479
+ */
480
+ classNames?: Partial<EditorClassNames>;
481
+ /**
482
+ * Imperative ref for programmatic control.
483
+ */
484
+ editorRef?: Ref<RichTextEditorRef>;
485
+ /**
486
+ * Whether the editor content is editable.
487
+ * @default true
488
+ */
489
+ editable?: boolean;
490
+ }
491
+ /**
492
+ * `RichTextEditor` is a TipTap-based editor for composing AT Protocol richtext.
493
+ *
494
+ * Features:
495
+ * - Real-time @mention autocomplete — defaults to the Bluesky public API,
496
+ * override with `onMentionQuery`
497
+ * - Automatic URL decoration (link facets detected on change)
498
+ * - Hard-break (Shift+Enter) for newlines inside a paragraph
499
+ * - Undo/redo history
500
+ * - `onChange` emits a `RichTextRecord` with `text` + `facets` populated via
501
+ * `detectFacetsWithoutResolution()`
502
+ * - Headless — bring your own CSS, or use the structural defaults in styles.css
503
+ *
504
+ * @example Basic usage (built-in Bluesky mention search)
505
+ * ```tsx
506
+ * <RichTextEditor
507
+ * placeholder="What's on your mind?"
508
+ * onChange={(record) => setPost(record)}
509
+ * />
510
+ * ```
511
+ *
512
+ * @example Custom mention search
513
+ * ```tsx
514
+ * <RichTextEditor
515
+ * placeholder="What's on your mind?"
516
+ * onMentionQuery={async (q) => searchProfiles(q)}
517
+ * onChange={(record) => setPost(record)}
518
+ * />
519
+ * ```
520
+ *
521
+ * @example With classNames
522
+ * ```tsx
523
+ * import { generateClassNames, defaultEditorClassNames } from 'bsky-richtext-react'
524
+ *
525
+ * <RichTextEditor
526
+ * classNames={generateClassNames([
527
+ * defaultEditorClassNames,
528
+ * { root: 'border rounded-lg p-3' },
529
+ * ], cn)}
530
+ * />
531
+ * ```
532
+ */
533
+ declare function RichTextEditor({ initialValue, onChange, placeholder, onFocus, onBlur, onMentionQuery, mentionSearchDebounceMs, disableDefaultMentionSearch, renderMentionSuggestion, mentionSuggestionOptions, classNames: classNamesProp, editorRef, editable, ...divProps }: RichTextEditorProps): react_jsx_runtime.JSX.Element;
534
+
535
+ /**
536
+ * Ref handle that TipTap calls into for keyboard events while the popup is open.
537
+ * Mirrors the `MentionListRef` interface from the Bluesky reference implementation.
538
+ */
539
+ interface MentionSuggestionListRef {
540
+ onKeyDown: (props: SuggestionKeyDownProps) => boolean;
541
+ }
542
+ interface MentionSuggestionListProps extends SuggestionProps<MentionSuggestion> {
543
+ /**
544
+ * Whether to render avatars when `avatarUrl` is present on a suggestion.
545
+ * When false, avatar placeholder is hidden entirely.
546
+ * @default true
547
+ */
548
+ showAvatars?: boolean;
549
+ /**
550
+ * Text to show when the items array is empty.
551
+ * @default "No results"
552
+ */
553
+ noResultsText?: string;
554
+ /**
555
+ * CSS class names for each styleable part of the suggestion dropdown.
556
+ *
557
+ * Use `generateClassNames()` to merge with the built-in defaults:
558
+ * @example
559
+ * ```tsx
560
+ * import { generateClassNames, defaultSuggestionClassNames } from 'bsky-richtext-react'
561
+ *
562
+ * classNames={generateClassNames([
563
+ * defaultSuggestionClassNames,
564
+ * { item: 'px-3 py-2', itemSelected: 'bg-blue-50' },
565
+ * ], cn)}
566
+ * ```
567
+ */
568
+ classNames?: Partial<SuggestionClassNames>;
569
+ }
570
+ /**
571
+ * Default mention suggestion dropdown rendered by `RichTextEditor`.
572
+ *
573
+ * Consumers can pass `classNames` to style specific parts, or pass a completely
574
+ * custom `renderMentionSuggestion` factory to the editor to replace this
575
+ * component entirely.
576
+ */
577
+ declare const MentionSuggestionList: react.ForwardRefExoticComponent<MentionSuggestionListProps & react.RefAttributes<MentionSuggestionListRef>>;
578
+
579
+ /**
580
+ * Parse a `RichTextRecord` into an array of `RichTextSegment` objects.
581
+ *
582
+ * The result is memoized — re-computation only occurs when `text` or the
583
+ * serialized facets change.
584
+ *
585
+ * @example
586
+ * ```tsx
587
+ * const segments = useRichText({ text: post.text, facets: post.facets })
588
+ * // [{ text: 'Hello ' }, { text: '@alice', feature: { $type: '...mention', did: '...' } }]
589
+ * ```
590
+ */
591
+ declare function useRichText(record: RichTextRecord): RichTextSegment[];
592
+
593
+ /**
594
+ * Richtext parser — converts `{ text, facets }` into an ordered array of
595
+ * `RichTextSegment` objects, each with their text slice and optional feature.
596
+ *
597
+ * Algorithm:
598
+ * 1. Sort facets by byteStart (ascending).
599
+ * 2. Walk through the text byte-by-byte, emitting plain segments between
600
+ * facets and annotated segments for each facet's byte range.
601
+ * 3. Overlapping facets are handled gracefully (skipped).
602
+ */
603
+
604
+ /**
605
+ * Parse a `RichTextRecord` into an ordered array of segments, each
606
+ * carrying its text and an optional facet feature.
607
+ *
608
+ * The segments are contiguous — joining all `segment.text` values
609
+ * reconstructs the original `record.text` exactly.
610
+ */
611
+ declare function parseRichText(record: RichTextRecord): RichTextSegment[];
612
+
613
+ /**
614
+ * URL display utilities for richtext rendering.
615
+ */
616
+ /**
617
+ * Shorten a URL for display purposes — strips the protocol and truncates
618
+ * the path if it's very long (mirrors Bluesky's `toShortUrl` behaviour).
619
+ *
620
+ * @example
621
+ * toShortUrl('https://example.com/some/very/long/path?q=foo')
622
+ * // => 'example.com/some/very/long/path?q=foo'
623
+ */
624
+ declare function toShortUrl(url: string, maxLength?: number): string;
625
+ /**
626
+ * Validate that a string is a well-formed URL.
627
+ */
628
+ declare function isValidUrl(url: string): boolean;
629
+
630
+ /**
631
+ * generateClassNames — deep-merge utility for component classNames objects.
632
+ *
633
+ * Accepts an array of partial classNames objects and merges them left-to-right,
634
+ * so later entries override earlier ones. String values at the same key are
635
+ * combined using the optional `cn` function (e.g. `clsx`, `tailwind-merge`).
636
+ * Nested objects (e.g. `suggestion` inside `EditorClassNames`) are recursively
637
+ * merged using the same rules.
638
+ *
639
+ * Falsy values in the array (`undefined`, `null`, `false`) are silently
640
+ * ignored, which makes conditional spreading ergonomic:
641
+ *
642
+ * @example Basic override
643
+ * ```ts
644
+ * import { generateClassNames, defaultEditorClassNames } from 'bsky-richtext-react'
645
+ *
646
+ * classNames={generateClassNames([
647
+ * defaultEditorClassNames,
648
+ * { root: 'border rounded-lg', mention: 'text-blue-500' },
649
+ * ])}
650
+ * ```
651
+ *
652
+ * @example With a Tailwind-merge / clsx utility
653
+ * ```ts
654
+ * import { cn } from '@/lib/utils'
655
+ *
656
+ * classNames={generateClassNames([
657
+ * defaultEditorClassNames,
658
+ * { root: 'rounded-none' },
659
+ * ], cn)}
660
+ * ```
661
+ *
662
+ * @example Conditional overrides
663
+ * ```ts
664
+ * classNames={generateClassNames([
665
+ * defaultEditorClassNames,
666
+ * isCompact && { root: 'text-sm' },
667
+ * isDark && darkThemeClassNames,
668
+ * ], cn)}
669
+ * ```
670
+ *
671
+ * @example Deep nested override (suggestion dropdown)
672
+ * ```ts
673
+ * classNames={generateClassNames([
674
+ * defaultEditorClassNames,
675
+ * { suggestion: { item: 'hover:bg-gray-100', itemSelected: 'bg-blue-50' } },
676
+ * ])}
677
+ * ```
678
+ */
679
+ /**
680
+ * A function that accepts any number of class strings (plus falsy values) and
681
+ * returns a single merged class string. Compatible with `clsx`, `classnames`,
682
+ * and `tailwind-merge`'s `twMerge` / `cn` utilities.
683
+ */
684
+ type ClassNameFn = (...inputs: Array<string | undefined | null | false>) => string;
685
+ /**
686
+ * Deep-merge an array of classNames objects into a single classNames object.
687
+ *
688
+ * @param classNamesArray - Objects to merge, left-to-right. Falsy entries are skipped.
689
+ * @param cn - Optional class utility function. When omitted, strings are
690
+ * joined with a single space (filtering empty strings).
691
+ */
692
+ declare function generateClassNames<T extends object>(classNamesArray: Array<Partial<T> | undefined | null | false>, cn?: ClassNameFn): T;
693
+
694
+ /**
695
+ * Bluesky public API helpers for mention search.
696
+ *
697
+ * `searchBskyActors` calls the unauthenticated public Bluesky API to look up
698
+ * actor suggestions. It is used as the default `onMentionQuery` implementation
699
+ * in `RichTextEditor` — no API key or authentication required.
700
+ *
701
+ * `createDebouncedSearch` wraps `searchBskyActors` with a debounce so rapid
702
+ * keystrokes don't fire unnecessary network requests. Only the latest in-flight
703
+ * query resolves; stale promises from earlier keystrokes are silently discarded.
704
+ */
705
+
706
+ /**
707
+ * Search for Bluesky actors using the public, unauthenticated API.
708
+ *
709
+ * Returns up to `limit` matching `MentionSuggestion` objects, or an empty
710
+ * array if the query is blank, the network request fails, or the response is
711
+ * malformed.
712
+ *
713
+ * @param query - Text the user typed after "@"
714
+ * @param limit - Max results to return (default: 8)
715
+ */
716
+ declare function searchBskyActors(query: string, limit?: number): Promise<MentionSuggestion[]>;
717
+ /**
718
+ * Create a debounced version of `searchBskyActors`.
719
+ *
720
+ * Rapid calls within `delayMs` are coalesced — only the *latest* invocation
721
+ * fires a network request. Earlier pending promises resolve with the same
722
+ * result as the latest call (they are not rejected or left dangling).
723
+ *
724
+ * @param delayMs - Debounce window in milliseconds (default: 300)
725
+ *
726
+ * @example
727
+ * ```ts
728
+ * const debouncedSearch = createDebouncedSearch(400)
729
+ * // Pass to onMentionQuery or use as the internal default
730
+ * ```
731
+ */
732
+ declare function createDebouncedSearch(delayMs?: number): (query: string) => Promise<MentionSuggestion[]>;
733
+
734
+ /**
735
+ * Default classNames for bsky-richtext-react components.
736
+ *
737
+ * These are the CSS class names used when no override is provided.
738
+ * The companion `styles.css` targets exactly these class names for structural
739
+ * layout rules. Consumers can extend or override them using `generateClassNames()`.
740
+ *
741
+ * @example Keep defaults, override one class
742
+ * ```tsx
743
+ * import { generateClassNames, defaultEditorClassNames } from 'bsky-richtext-react'
744
+ *
745
+ * classNames={generateClassNames([
746
+ * defaultEditorClassNames,
747
+ * { root: 'border rounded-lg p-2' },
748
+ * ])}
749
+ * ```
750
+ *
751
+ * @example Completely replace defaults (opt out of all built-in class names)
752
+ * ```tsx
753
+ * classNames={{ root: 'my-editor', mention: 'my-mention' }}
754
+ * ```
755
+ */
756
+
757
+ declare const defaultDisplayClassNames: DisplayClassNames;
758
+ declare const defaultSuggestionClassNames: SuggestionClassNames;
759
+ declare const defaultEditorClassNames: EditorClassNames;
760
+
761
+ export { type ByteSlice, type ClassNameFn, type DefaultSuggestionRendererOptions, type DisplayClassNames, type EditorClassNames, type Facet, type FacetFeature, type LinkFeature, type LinkProps, type MentionFeature, type MentionProps, type MentionSuggestion, MentionSuggestionList, type MentionSuggestionListProps, type MentionSuggestionListRef, RichTextDisplay, type RichTextDisplayProps, RichTextEditor, type RichTextEditorProps, type RichTextEditorRef, type RichTextRecord, type RichTextSegment, type SuggestionClassNames, type TagFeature, type TagProps, createDebouncedSearch, createDefaultSuggestionRenderer, defaultDisplayClassNames, defaultEditorClassNames, defaultSuggestionClassNames, generateClassNames, isLinkFeature, isMentionFeature, isTagFeature, isValidUrl, parseRichText, searchBskyActors, toShortUrl, useRichText };