bsky-richtext-react 1.0.2 → 2.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 CHANGED
@@ -7,6 +7,74 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ---
9
9
 
10
+ ## [2.0.0] — 2026-02-18
11
+
12
+ ### Breaking Changes
13
+
14
+ - **Upgraded all `@tiptap/*` dependencies from v2 to v3** (`^3.20.0`). Consumers must update their installed `@tiptap/*` packages to v3.
15
+ - **Replaced `tippy.js` with `@floating-ui/dom`** for mention suggestion popup positioning, aligning with the tiptap v3 ecosystem. The `tippy.js` peer dependency has been removed.
16
+ - **Peer dependency requirements changed**: consumers must install `@tiptap/*@^3.20.0` and `@floating-ui/dom@^1.6.0`. The `tippy.js` peer dependency is no longer required.
17
+
18
+ ### Why
19
+
20
+ - Fixes Turbopack build failures in Next.js 16+ when the consumer's dependency tree includes tiptap v3 packages (e.g., via `@tiptap/starter-kit@3.x`). Turbopack's strict static export analysis caught that `@tiptap/extension-list@3.x` imports symbols from `@tiptap/core` that don't exist in v2.
21
+ - Aligns with the tiptap v3 ecosystem (`tippy.js` → `@floating-ui/dom`).
22
+
23
+ ### Migration
24
+
25
+ ```diff
26
+ - "@tiptap/core": "^2.x.x",
27
+ + "@tiptap/core": "^3.20.0",
28
+ // repeat for all @tiptap/* packages
29
+
30
+ - "tippy.js": "^6.x.x",
31
+ + "@floating-ui/dom": "^1.6.0",
32
+ ```
33
+
34
+ ---
35
+
36
+ ## [2.0.0] — 2026-02-18
37
+
38
+ ### Breaking Changes
39
+
40
+ - **tiptap upgraded from v2 to v3** — all `@tiptap/*` peer dependencies must be updated to `^3.20.0`.
41
+ - **`tippy.js` replaced with `@floating-ui/dom`** — the mention suggestion popup now uses `@floating-ui/dom` for positioning. Remove `tippy.js` from your dependencies and install `@floating-ui/dom` instead.
42
+
43
+ ### Migration
44
+
45
+ ```diff
46
+ - bun add @tiptap/core@^2 @tiptap/react@^2 ... tippy.js
47
+ + bun add @tiptap/core@^3.20.0 @tiptap/react@^3.20.0 ... @floating-ui/dom
48
+ ```
49
+
50
+ Update all `@tiptap/*` peer dependencies to `^3.20.0` and replace `tippy.js` with `@floating-ui/dom`.
51
+
52
+ ---
53
+
54
+ ## [1.1.0] — 2026-02-17
55
+
56
+ ### Changed
57
+
58
+ - **Default classNames now use Tailwind CSS utility classes** instead of the previous BEM-style class names (`bsky-editor`, `bsky-richtext`, etc.). All components have sensible out-of-the-box appearance as long as Tailwind is configured in the consumer's project — no extra setup needed.
59
+ - `defaultDisplayClassNames` — mentions, links, and tags are styled `text-blue-500 hover:underline`.
60
+ - `defaultEditorClassNames` — editor root is `block w-full relative`; mention chips and autolinks are `inline text-blue-500`.
61
+ - `defaultSuggestionClassNames` — dropdown has full visual defaults: `bg-white rounded-lg shadow-lg border`, hover states, avatar layout, truncated text columns.
62
+ - **`styles.css` removed from the published package.** The `bsky-richtext-react/styles.css` sub-export and the `./dist/styles.css` artefact no longer exist. Remove any `import 'bsky-richtext-react/styles.css'` from your app.
63
+ - **`sideEffects` set to `false`** — the package no longer has CSS side effects, enabling full tree-shaking.
64
+ - **Tailwind added as a `devDependency`** — used exclusively in Storybook for development/testing. Not bundled in or required by consumers.
65
+ - The `classNames` prop API and `generateClassNames()` are unchanged — all existing override patterns continue to work.
66
+
67
+ ### Migration
68
+
69
+ ```diff
70
+ - import 'bsky-richtext-react/styles.css'
71
+ import { RichTextEditor } from 'bsky-richtext-react'
72
+ ```
73
+
74
+ If you were targeting the old BEM class names in your own CSS, replace them with the `classNames` prop or `generateClassNames()` to apply your own classes directly.
75
+
76
+ ---
77
+
10
78
  ## [1.0.2] — 2026-02-17
11
79
 
12
80
  ### Fixed
package/README.md CHANGED
@@ -14,7 +14,7 @@
14
14
  - **`<RichTextDisplay>`** — Render AT Protocol richtext records (`text` + `facets`) as interactive HTML. Handles @mentions, links, and #hashtags with fully customisable renderers and URL resolvers.
15
15
  - **`<RichTextEditor>`** — TipTap-based editor with real-time @mention autocomplete (powered by the **Bluesky public API** by default — no auth required), stateless URL decoration, undo/redo, and an imperative ref API.
16
16
  - **`generateClassNames()`** — Deep-merge utility for the `classNames` prop system. Pass an array of partial classNames objects and get one merged result, optionally using your own `cn()` / `clsx` / `tailwind-merge` utility.
17
- - **Headless by design** — Ships with layout-only CSS. Bring your own colours, fonts, and borders.
17
+ - **Tailwind defaults, fully overridable** — Default classNames use Tailwind utility classes out of the box. Override any part via the `classNames` prop — no stylesheet import needed.
18
18
  - **Fully typed** — TypeScript-first with complete type definitions for all AT Protocol facet types.
19
19
  - **Tree-shakeable** — ESM + CJS dual build via `tsup`.
20
20
 
@@ -36,7 +36,16 @@ pnpm add bsky-richtext-react
36
36
  Peer dependencies (if not already installed):
37
37
 
38
38
  ```bash
39
- bun add react react-dom
39
+ bun add react react-dom @floating-ui/dom \
40
+ @tiptap/core@^3.20.0 \
41
+ @tiptap/extension-document@^3.20.0 \
42
+ @tiptap/extension-hard-break@^3.20.0 \
43
+ @tiptap/extension-history@^3.20.0 \
44
+ @tiptap/extension-mention@^3.20.0 \
45
+ @tiptap/extension-paragraph@^3.20.0 \
46
+ @tiptap/extension-placeholder@^3.20.0 \
47
+ @tiptap/extension-text@^3.20.0 \
48
+ @tiptap/react@^3.20.0
40
49
  ```
41
50
 
42
51
  ---
@@ -47,7 +56,6 @@ bun add react react-dom
47
56
 
48
57
  ```tsx
49
58
  import { RichTextDisplay } from 'bsky-richtext-react'
50
- import 'bsky-richtext-react/styles.css'
51
59
 
52
60
  // Pass raw fields from an app.bsky.feed.post record
53
61
  export function Post({ post }) {
@@ -59,7 +67,6 @@ export function Post({ post }) {
59
67
 
60
68
  ```tsx
61
69
  import { RichTextEditor } from 'bsky-richtext-react'
62
- import 'bsky-richtext-react/styles.css'
63
70
 
64
71
  export function Composer() {
65
72
  return (
@@ -81,84 +88,13 @@ export function Composer() {
81
88
 
82
89
  ## Styling
83
90
 
84
- ### 1. Import the structural CSS
91
+ The library ships **no CSS file**. All default styles are Tailwind utility classes applied through the `classNames` prop system. As long as Tailwind is configured in your project, components look good with zero extra setup.
85
92
 
86
- ```ts
87
- import 'bsky-richtext-react/styles.css'
88
- ```
93
+ ### 1. Defaults — Tailwind utility classes
89
94
 
90
- This only sets `display`, `word-break`, `box-sizing`, and flex layout rules. **No colours, fonts, or borders are applied.** You control all visual styling.
91
-
92
- ### 2. Target the default class names
93
-
94
- Every element rendered by the components carries a predictable CSS class that you can target directly:
95
-
96
- #### `<RichTextDisplay>`
97
-
98
- | Class | Element |
99
- |-------|---------|
100
- | `.bsky-richtext` | Root `<span>` |
101
- | `.bsky-mention` | @mention `<a>` |
102
- | `.bsky-link` | Link `<a>` |
103
- | `.bsky-tag` | #hashtag `<a>` |
104
-
105
- #### `<RichTextEditor>`
106
-
107
- | Class | Element |
108
- |-------|---------|
109
- | `.bsky-editor` | Root wrapper `<div>` |
110
- | `.bsky-editor-content` | ProseMirror wrapper |
111
- | `.bsky-editor-mention` | Mention chip inside editor |
112
- | `.autolink` | URL decoration span inside editor |
113
-
114
- #### `<MentionSuggestionList>` (dropdown)
115
-
116
- | Class | Element |
117
- |-------|---------|
118
- | `.bsky-suggestions` | Outer container |
119
- | `.bsky-suggestion-item` | Each suggestion row (`<button>`) |
120
- | `.bsky-suggestion-item-selected` | Currently highlighted row |
121
- | `.bsky-suggestion-avatar` | Avatar wrapper |
122
- | `.bsky-suggestion-avatar-img` | `<img>` element |
123
- | `.bsky-suggestion-avatar-placeholder` | Initial-letter fallback |
124
- | `.bsky-suggestion-text` | Text column (name + handle) |
125
- | `.bsky-suggestion-name` | Display name |
126
- | `.bsky-suggestion-handle` | `@handle` |
127
- | `.bsky-suggestion-empty` | "No results" message |
128
-
129
- ```css
130
- /* Example: style the editor */
131
- .bsky-editor .ProseMirror {
132
- padding: 12px 16px;
133
- border: 1px solid #e1e4e8;
134
- border-radius: 8px;
135
- font-size: 16px;
136
- line-height: 1.5;
137
- min-height: 120px;
138
- }
95
+ Every component has a set of default Tailwind classes applied out of the box (see `defaultEditorClassNames`, `defaultDisplayClassNames`, `defaultSuggestionClassNames`). No stylesheet import is required.
139
96
 
140
- /* Example: style mentions in a post */
141
- .bsky-mention {
142
- color: #0085ff;
143
- font-weight: 600;
144
- text-decoration: none;
145
- }
146
- .bsky-mention:hover { text-decoration: underline; }
147
-
148
- /* Example: style the suggestion dropdown */
149
- .bsky-suggestions {
150
- background: #fff;
151
- border: 1px solid #e1e4e8;
152
- border-radius: 8px;
153
- box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
154
- min-width: 240px;
155
- padding: 4px;
156
- }
157
- .bsky-suggestion-item { padding: 8px 10px; border-radius: 6px; }
158
- .bsky-suggestion-item-selected { background: #f0f4ff; }
159
- ```
160
-
161
- ### 3. Use `generateClassNames()` for targeted overrides
97
+ ### 2. Use `generateClassNames()` for targeted overrides
162
98
 
163
99
  The `classNames` prop on each component accepts a nested object. Use `generateClassNames()` to cleanly layer your classes on top of the defaults without rewriting them from scratch:
164
100
 
@@ -205,7 +141,9 @@ Pass a plain object to skip the defaults entirely:
205
141
  <RichTextDisplay classNames={{ root: 'my-text', mention: 'my-mention' }} />
206
142
  ```
207
143
 
208
- ### 4. Tailwind integration
144
+ ### 3. Using `tailwind-merge` to deduplicate classes
145
+
146
+ When layering your own Tailwind classes on top of the defaults, use `tailwind-merge` as the `cn` argument to avoid conflicting class duplication:
209
147
 
210
148
  ```tsx
211
149
  import { twMerge } from 'tailwind-merge'
@@ -295,7 +233,7 @@ import { RichTextEditor } from 'bsky-richtext-react'
295
233
  | `onMentionQuery` | `(query: string) => Promise<MentionSuggestion[]>` | **Bluesky public API** | Custom mention search. Overrides the built-in search. |
296
234
  | `mentionSearchDebounceMs` | `number` | `300` | Debounce delay (ms) for the built-in search. No effect when `onMentionQuery` is set. |
297
235
  | `disableDefaultMentionSearch` | `boolean` | `false` | Disable the built-in Bluesky API search entirely |
298
- | `renderMentionSuggestion` | `SuggestionOptions['render']` | tippy.js popup | Custom TipTap suggestion renderer factory |
236
+ | `renderMentionSuggestion` | `SuggestionOptions['render']` | @floating-ui/dom popup | Custom TipTap suggestion renderer factory |
299
237
  | `mentionSuggestionOptions` | `DefaultSuggestionRendererOptions` | — | Options forwarded to the default renderer |
300
238
  | `editorRef` | `Ref<RichTextEditorRef>` | — | Imperative ref |
301
239
  | `editable` | `boolean` | `true` | Toggle read-only mode |
@@ -352,7 +290,7 @@ The editor searches Bluesky actors **by default** when the user types `@`:
352
290
 
353
291
  ### `<MentionSuggestionList>`
354
292
 
355
- The default suggestion dropdown rendered inside the tippy.js popup. Exported so you can reuse or reference it in your own popup implementation.
293
+ The default suggestion dropdown rendered inside the @floating-ui/dom popup. Exported so you can reuse or reference it in your own popup implementation.
356
294
 
357
295
  ```tsx
358
296
  import { MentionSuggestionList } from 'bsky-richtext-react'
@@ -578,6 +516,24 @@ bun run format
578
516
 
579
517
  ---
580
518
 
519
+ ## Migration
520
+
521
+ ### v2.0.0 — tiptap v3 upgrade
522
+
523
+ **v2.0.0 upgrades tiptap from v2 to v3.** If you are upgrading from v1.x, you must:
524
+
525
+ 1. Update all `@tiptap/*` peer dependencies to `^3.20.0`.
526
+ 2. Replace `tippy.js` with `@floating-ui/dom`.
527
+
528
+ ```diff
529
+ - bun add @tiptap/core@^2 @tiptap/react@^2 tippy.js
530
+ + bun add @tiptap/core@^3.20.0 @tiptap/react@^3.20.0 @floating-ui/dom
531
+ ```
532
+
533
+ See [CHANGELOG.md](./CHANGELOG.md) for the full list of changes.
534
+
535
+ ---
536
+
581
537
  ## Changelog
582
538
 
583
539
  See [CHANGELOG.md](./CHANGELOG.md).
package/dist/index.cjs CHANGED
@@ -11,15 +11,11 @@ var extensionHardBreak = require('@tiptap/extension-hard-break');
11
11
  var extensionPlaceholder = require('@tiptap/extension-placeholder');
12
12
  var api = require('@atproto/api');
13
13
  var extensionMention = require('@tiptap/extension-mention');
14
- var tippy = require('tippy.js');
14
+ var dom = require('@floating-ui/dom');
15
15
  var core = require('@tiptap/core');
16
16
  var state = require('@tiptap/pm/state');
17
17
  var view = require('@tiptap/pm/view');
18
18
 
19
- function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
20
-
21
- var tippy__default = /*#__PURE__*/_interopDefault(tippy);
22
-
23
19
  // src/components/RichTextDisplay/RichTextDisplay.tsx
24
20
 
25
21
  // src/types/facets.ts
@@ -35,29 +31,28 @@ function isTagFeature(feature) {
35
31
 
36
32
  // src/defaults/classNames.ts
37
33
  var defaultDisplayClassNames = {
38
- root: "bsky-richtext",
39
- mention: "bsky-mention",
40
- link: "bsky-link",
41
- tag: "bsky-tag"
34
+ root: "inline break-words",
35
+ mention: "inline text-blue-500 hover:underline cursor-pointer",
36
+ link: "inline text-blue-500 hover:underline",
37
+ tag: "inline text-blue-500 hover:underline cursor-pointer"
42
38
  };
43
39
  var defaultSuggestionClassNames = {
44
- root: "bsky-suggestions",
45
- item: "bsky-suggestion-item",
46
- itemSelected: "bsky-suggestion-item-selected",
47
- avatar: "bsky-suggestion-avatar",
48
- avatarImg: "bsky-suggestion-avatar-img",
49
- avatarPlaceholder: "bsky-suggestion-avatar-placeholder",
50
- text: "bsky-suggestion-text",
51
- name: "bsky-suggestion-name",
52
- handle: "bsky-suggestion-handle",
53
- empty: "bsky-suggestion-empty"
40
+ root: "flex flex-col max-h-80 overflow-y-auto bg-white rounded-lg shadow-lg border border-gray-200 min-w-60",
41
+ item: "flex items-center gap-3 w-full px-3 py-2 text-left cursor-pointer border-none bg-transparent hover:bg-gray-100 select-none",
42
+ itemSelected: "bg-gray-100",
43
+ avatar: "flex-shrink-0 w-10 h-10 rounded-full overflow-hidden bg-gray-200 flex items-center justify-center",
44
+ avatarImg: "block w-full h-full object-cover",
45
+ avatarPlaceholder: "flex items-center justify-center w-full h-full text-gray-500 font-medium text-sm",
46
+ text: "flex flex-col flex-1 min-w-0 overflow-hidden",
47
+ name: "block truncate font-medium text-gray-900 text-sm",
48
+ handle: "block truncate text-xs text-gray-500",
49
+ empty: "block px-3 py-2 text-sm text-gray-500"
54
50
  };
55
51
  var defaultEditorClassNames = {
56
- root: "bsky-editor",
57
- content: "bsky-editor-content",
58
- placeholder: "bsky-editor-placeholder",
59
- mention: "bsky-editor-mention",
60
- link: "autolink",
52
+ root: "block w-full relative",
53
+ content: "block w-full",
54
+ mention: "inline text-blue-500",
55
+ link: "inline text-blue-500",
61
56
  suggestion: defaultSuggestionClassNames
62
57
  };
63
58
 
@@ -372,13 +367,7 @@ function createDebouncedSearch(delayMs = 300) {
372
367
  });
373
368
  };
374
369
  }
375
- var MentionSuggestionList = react.forwardRef(function MentionSuggestionListImpl({
376
- items,
377
- command,
378
- showAvatars = true,
379
- noResultsText = "No results",
380
- classNames: classNamesProp
381
- }, ref) {
370
+ var MentionSuggestionList = react.forwardRef(function MentionSuggestionListImpl({ items, command, showAvatars = true, noResultsText = "No results", classNames: classNamesProp }, ref) {
382
371
  const [selectedIndex, setSelectedIndex] = react.useState(0);
383
372
  const cn = react.useMemo(
384
373
  () => generateClassNames([defaultSuggestionClassNames, classNamesProp]),
@@ -462,12 +451,6 @@ function createDefaultSuggestionRenderer(options = {}) {
462
451
  return () => {
463
452
  let renderer;
464
453
  let popup;
465
- const destroy = () => {
466
- popup?.[0]?.destroy();
467
- renderer?.destroy();
468
- renderer = void 0;
469
- popup = void 0;
470
- };
471
454
  const buildProps = (props) => ({
472
455
  ...props,
473
456
  showAvatars: options.showAvatars ?? true,
@@ -482,33 +465,49 @@ function createDefaultSuggestionRenderer(options = {}) {
482
465
  });
483
466
  if (!props.clientRect) return;
484
467
  const clientRect = props.clientRect;
485
- popup = tippy__default.default("body", {
486
- getReferenceClientRect: () => clientRect?.() ?? new DOMRect(),
487
- appendTo: () => document.body,
488
- content: renderer.element,
489
- showOnCreate: true,
490
- interactive: true,
491
- trigger: "manual",
492
- placement: "bottom-start"
468
+ popup = document.createElement("div");
469
+ popup.style.position = "fixed";
470
+ popup.style.zIndex = "9999";
471
+ popup.appendChild(renderer.element);
472
+ document.body.appendChild(popup);
473
+ const virtualEl = { getBoundingClientRect: () => clientRect?.() ?? new DOMRect() };
474
+ void dom.computePosition(virtualEl, popup, {
475
+ placement: "bottom-start",
476
+ middleware: [dom.offset(8), dom.flip(), dom.shift({ padding: 8 })]
477
+ }).then(({ x, y }) => {
478
+ if (popup) {
479
+ popup.style.left = `${x}px`;
480
+ popup.style.top = `${y}px`;
481
+ }
493
482
  });
494
483
  },
495
484
  onUpdate(props) {
496
485
  renderer?.updateProps(buildProps(props));
497
- if (!props.clientRect) return;
486
+ if (!props.clientRect || !popup) return;
498
487
  const clientRect = props.clientRect;
499
- popup?.[0]?.setProps({
500
- getReferenceClientRect: () => clientRect?.() ?? new DOMRect()
488
+ const virtualEl = { getBoundingClientRect: () => clientRect?.() ?? new DOMRect() };
489
+ void dom.computePosition(virtualEl, popup, {
490
+ placement: "bottom-start",
491
+ middleware: [dom.offset(8), dom.flip(), dom.shift({ padding: 8 })]
492
+ }).then(({ x, y }) => {
493
+ if (popup) {
494
+ popup.style.left = `${x}px`;
495
+ popup.style.top = `${y}px`;
496
+ }
501
497
  });
502
498
  },
503
499
  onKeyDown(props) {
504
500
  if (props.event.key === "Escape") {
505
- popup?.[0]?.hide();
501
+ if (popup) popup.style.display = "none";
506
502
  return true;
507
503
  }
508
504
  return renderer?.ref?.onKeyDown(props) ?? false;
509
505
  },
510
506
  onExit() {
511
- destroy();
507
+ popup?.remove();
508
+ renderer?.destroy();
509
+ popup = void 0;
510
+ renderer = void 0;
512
511
  }
513
512
  };
514
513
  };