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.
- package/CHANGELOG.md +101 -0
- package/README.md +589 -0
- package/dist/index.cjs +5676 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +761 -0
- package/dist/index.d.ts +761 -0
- package/dist/index.js +5654 -0
- package/dist/index.js.map +1 -0
- package/dist/styles.css +106 -0
- package/dist/styles.css.map +1 -0
- package/dist/styles.d.cts +2 -0
- package/dist/styles.d.ts +2 -0
- package/package.json +126 -0
package/dist/index.d.ts
ADDED
|
@@ -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 };
|