@pilotiq/tiptap 3.19.0 → 3.19.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/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # @pilotiq/tiptap
2
2
 
3
+ ## 3.19.1
4
+
5
+ ### Patch Changes
6
+
7
+ - ff44b8d: Show the inline mark toolbar on a bare caret inside a formatting mark.
8
+
9
+ The selection-based `FloatingToolbar` only appeared on a non-empty text selection, so a link or bold span couldn't be edited by just clicking into it (#156). Its show/hide decision now lives in a pure, exported `shouldShowFloatingToolbar(state)` predicate: a caret (empty selection) surfaces the toolbar when it sits inside one of the toolbar's marks (`bold` / `italic` / `strike` / `code` / `link`), non-empty ranges behave as before, and the callout/alert suppression from #155 still holds. Pinned by `floatingToolbarVisibility.dom.test.ts` against the real schema.
10
+
3
11
  ## 3.19.0
4
12
 
5
13
  ### Minor Changes
package/dist/index.d.ts CHANGED
@@ -12,4 +12,5 @@ export { AiInlineDiffExtension, aiInlineDiffPluginKey, getAiInlineDiffState, typ
12
12
  export { planReplaceBlock, planInsertBlockBefore, planDeleteBlock, planWrapBlocks, planUpdateBlockMark, summarizeBlockStructure, type BlockMarkRange, type TransactionModifier, } from './surgicalOps.js';
13
13
  export { renderRichTextToHtml, isRichTextValue, type RenderRichTextOptions, type TiptapNode, type TiptapMark, } from './render.js';
14
14
  export { contentBlockNodes, Intro, Faq, FaqItem, FaqQuestion, FaqAnswer, Alert, AlertTitle, AlertBody, Summary, KeyTakeaways, ProsCons, ProsColumn, ConsColumn, ContentBlockKeymap, LabeledBlockExitKeymap, planExitLabeledBlock, isSelectionInAlert, ALERT_VARIANTS, ALERT_VARIANT_LABEL, coerceAlertType, type AlertType, } from './extensions/contentBlocks.js';
15
+ export { shouldShowFloatingToolbar, TOOLBAR_MARKS } from './react/floatingToolbarVisibility.js';
15
16
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -17,3 +17,4 @@ export { renderRichTextToHtml, isRichTextValue, } from './render.js';
17
17
  // editor — e.g. to parse the content-block HTML or drive the surgical-op
18
18
  // planners (`planInsertBlockBefore` & co.) in a test, without mounting React.
19
19
  export { contentBlockNodes, Intro, Faq, FaqItem, FaqQuestion, FaqAnswer, Alert, AlertTitle, AlertBody, Summary, KeyTakeaways, ProsCons, ProsColumn, ConsColumn, ContentBlockKeymap, LabeledBlockExitKeymap, planExitLabeledBlock, isSelectionInAlert, ALERT_VARIANTS, ALERT_VARIANT_LABEL, coerceAlertType, } from './extensions/contentBlocks.js';
20
+ export { shouldShowFloatingToolbar, TOOLBAR_MARKS } from './react/floatingToolbarVisibility.js';
@@ -3,10 +3,12 @@ interface FloatingToolbarProps {
3
3
  editor: Editor;
4
4
  }
5
5
  /**
6
- * Selection-based formatting toolbar. Visible whenever the editor has a
7
- * non-empty range selection inside text content. Inline marks (B/I/S/Code)
8
- * are grouped together; Link sits after a separator since it's a different
9
- * kind of action.
6
+ * Selection-based formatting toolbar. Visible on a non-empty range inside
7
+ * text content, AND on a bare caret sitting inside a formatting mark (so a
8
+ * link / bold span can be edited without selecting it first — #156). Inline
9
+ * marks (B/I/S/Code) are grouped together; Link sits after a separator since
10
+ * it's a different kind of action. Visibility is decided by the pure
11
+ * `shouldShowFloatingToolbar` predicate; this component only positions.
10
12
  */
11
13
  export declare function FloatingToolbar({ editor }: FloatingToolbarProps): import("react").JSX.Element;
12
14
  export {};
@@ -2,12 +2,14 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
2
2
  import { useEffect, useState } from 'react';
3
3
  import { Tooltip } from '@base-ui/react/tooltip';
4
4
  import { Dialog } from '@base-ui/react/dialog';
5
- import { isSelectionInAlert } from '../extensions/contentBlocks.js';
5
+ import { shouldShowFloatingToolbar } from './floatingToolbarVisibility.js';
6
6
  /**
7
- * Selection-based formatting toolbar. Visible whenever the editor has a
8
- * non-empty range selection inside text content. Inline marks (B/I/S/Code)
9
- * are grouped together; Link sits after a separator since it's a different
10
- * kind of action.
7
+ * Selection-based formatting toolbar. Visible on a non-empty range inside
8
+ * text content, AND on a bare caret sitting inside a formatting mark (so a
9
+ * link / bold span can be edited without selecting it first — #156). Inline
10
+ * marks (B/I/S/Code) are grouped together; Link sits after a separator since
11
+ * it's a different kind of action. Visibility is decided by the pure
12
+ * `shouldShowFloatingToolbar` predicate; this component only positions.
11
13
  */
12
14
  export function FloatingToolbar({ editor }) {
13
15
  const [pos, setPos] = useState(null);
@@ -15,24 +17,13 @@ export function FloatingToolbar({ editor }) {
15
17
  const [linkUrl, setLinkUrl] = useState('');
16
18
  useEffect(() => {
17
19
  const update = () => {
18
- const { from, to, empty } = editor.state.selection;
19
- if (empty) {
20
- setPos(null);
21
- return;
22
- }
23
- // The callout/alert block owns its content + chrome (the in-block gear
24
- // menu); the inline mark toolbar shouldn't appear inside it — or when the
25
- // whole block is picked via the drag handle (a NodeSelection) (#155).
26
- if (isSelectionInAlert(editor.state.selection)) {
27
- setPos(null);
28
- return;
29
- }
30
- // Don't show on full-block selections (e.g. clicking a custom block).
31
- const slice = editor.state.doc.slice(from, to);
32
- if (slice.content.childCount === 0) {
20
+ if (!shouldShowFloatingToolbar(editor.state)) {
33
21
  setPos(null);
34
22
  return;
35
23
  }
24
+ const { from, to } = editor.state.selection;
25
+ // For a bare caret `from === to`, so both coords resolve to the caret
26
+ // point and the toolbar centers above it.
36
27
  const start = editor.view.coordsAtPos(from);
37
28
  const end = editor.view.coordsAtPos(to);
38
29
  // Viewport-relative — pair with `position: fixed` below. The wrapper
@@ -0,0 +1,20 @@
1
+ import type { EditorState } from '@tiptap/pm/state';
2
+ /**
3
+ * Inline marks the `FloatingToolbar` can toggle — bold / italic / strike /
4
+ * code / link. A caret (empty selection) sitting inside any of these surfaces
5
+ * the toolbar so the formatting can be edited in place, without first
6
+ * selecting the text (#156).
7
+ */
8
+ export declare const TOOLBAR_MARKS: readonly ["bold", "italic", "strike", "code", "link"];
9
+ /**
10
+ * Whether the selection-based `FloatingToolbar` should be visible. A PURE
11
+ * decision (no DOM / coords) so it's unit-testable against the real schema:
12
+ *
13
+ * - never inside a callout/alert block — it owns its own chrome (#155);
14
+ * - a caret (empty selection) shows ONLY when it sits inside a formatting
15
+ * mark (link / bold / …) so the mark can be edited without selecting (#156);
16
+ * - a non-empty range shows whenever it actually spans inline content (the
17
+ * `childCount === 0` guard skips degenerate full-block selections).
18
+ */
19
+ export declare function shouldShowFloatingToolbar(state: EditorState): boolean;
20
+ //# sourceMappingURL=floatingToolbarVisibility.d.ts.map
@@ -0,0 +1,39 @@
1
+ import { isSelectionInAlert } from '../extensions/contentBlocks.js';
2
+ /**
3
+ * Inline marks the `FloatingToolbar` can toggle — bold / italic / strike /
4
+ * code / link. A caret (empty selection) sitting inside any of these surfaces
5
+ * the toolbar so the formatting can be edited in place, without first
6
+ * selecting the text (#156).
7
+ */
8
+ export const TOOLBAR_MARKS = ['bold', 'italic', 'strike', 'code', 'link'];
9
+ const TOOLBAR_MARK_SET = new Set(TOOLBAR_MARKS);
10
+ /**
11
+ * True when the caret (an EMPTY selection) sits inside one of the toolbar's
12
+ * formatting marks. Reads `storedMarks` first (so the active mark at a typed
13
+ * boundary still counts), then the marks resolved at the cursor.
14
+ */
15
+ function caretInToolbarMark(state) {
16
+ const sel = state.selection;
17
+ if (!sel.empty)
18
+ return false;
19
+ const marks = state.storedMarks ?? sel.$from.marks();
20
+ return marks.some((m) => TOOLBAR_MARK_SET.has(m.type.name));
21
+ }
22
+ /**
23
+ * Whether the selection-based `FloatingToolbar` should be visible. A PURE
24
+ * decision (no DOM / coords) so it's unit-testable against the real schema:
25
+ *
26
+ * - never inside a callout/alert block — it owns its own chrome (#155);
27
+ * - a caret (empty selection) shows ONLY when it sits inside a formatting
28
+ * mark (link / bold / …) so the mark can be edited without selecting (#156);
29
+ * - a non-empty range shows whenever it actually spans inline content (the
30
+ * `childCount === 0` guard skips degenerate full-block selections).
31
+ */
32
+ export function shouldShowFloatingToolbar(state) {
33
+ if (isSelectionInAlert(state.selection))
34
+ return false;
35
+ if (state.selection.empty)
36
+ return caretInToolbarMark(state);
37
+ const { from, to } = state.selection;
38
+ return state.doc.slice(from, to).content.childCount > 0;
39
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pilotiq/tiptap",
3
- "version": "3.19.0",
3
+ "version": "3.19.1",
4
4
  "description": "Tiptap rich-text editor adapter for @pilotiq/pilotiq — slash menu, draggable blocks, custom-block API",
5
5
  "license": "MIT",
6
6
  "repository": {