@pilotiq/tiptap 3.15.0 → 3.15.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,20 @@
1
1
  # @pilotiq/tiptap
2
2
 
3
+ ## 3.15.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 2bc1e54: Slash menu now ranks results by relevance instead of definition order.
8
+
9
+ A query that matches an entry's **label** (exactly, by prefix, or by word) now
10
+ ranks above an entry that only mentions the word in its `searchKey`. Previously
11
+ the menu was a plain substring filter that preserved definition order, so typing
12
+ `/summary` surfaced **Collapsible block** first — its `searchKey` lists
13
+ "summary" and it's defined before the Summary block — and pressing Enter
14
+ inserted the wrong block. The matched set is unchanged (every entry that matched
15
+ before still matches); only the ordering improves. Ties keep their original menu
16
+ order.
17
+
3
18
  ## 3.15.0
4
19
 
5
20
  ### Minor Changes
@@ -298,7 +298,46 @@ export function buildSlashItems(blocks, mergeTags, query, insert) {
298
298
  if (!query)
299
299
  return all;
300
300
  const needle = query.toLowerCase();
301
- return all.filter((item) => `${item.label} ${item.searchKey} ${item.group ?? ''}`.toLowerCase().includes(needle));
301
+ // Membership is unchanged from a plain substring match (an item shows iff the
302
+ // query appears anywhere in its label / searchKey / group). On top of that we
303
+ // RANK by relevance so a query that hits an item's LABEL beats one that only
304
+ // mentions the word in some other item's searchKey — otherwise "/summary"
305
+ // surfaces "Collapsible block" first (its searchKey lists "summary") instead
306
+ // of the Summary block. Stable: equal scores keep their original menu order.
307
+ return all
308
+ .map((item, i) => ({ item, i, score: slashRelevance(item, needle) }))
309
+ .filter((m) => m.score > 0)
310
+ .sort((a, b) => b.score - a.score || a.i - b.i)
311
+ .map((m) => m.item);
312
+ }
313
+ /**
314
+ * Score a slash item against the lowercased query. Higher = more relevant.
315
+ * Returns 0 when nothing matches (the item is filtered out). The tiers mirror
316
+ * how people expect a command palette to rank: an exact/prefix LABEL hit wins,
317
+ * then any label word, then label substring, then searchKey words, then a bare
318
+ * searchKey/group substring. `needle` is assumed already lowercased.
319
+ */
320
+ function slashRelevance(item, needle) {
321
+ const label = item.label.toLowerCase();
322
+ const labelWords = label.split(/\s+/).filter(Boolean);
323
+ if (label === needle)
324
+ return 7;
325
+ if (label.startsWith(needle))
326
+ return 6;
327
+ if (labelWords.some((w) => w.startsWith(needle)))
328
+ return 5;
329
+ if (label.includes(needle))
330
+ return 4;
331
+ const searchWords = item.searchKey.toLowerCase().split(/\s+/).filter(Boolean);
332
+ if (searchWords.some((w) => w === needle))
333
+ return 3;
334
+ if (searchWords.some((w) => w.startsWith(needle)))
335
+ return 2;
336
+ // Matched only via a searchKey / group substring (no word boundary) — still
337
+ // shown, but ranked below every label and word-boundary hit.
338
+ if (`${item.searchKey} ${item.group ?? ''}`.toLowerCase().includes(needle))
339
+ return 1;
340
+ return 0;
302
341
  }
303
342
  function defaultsFromSchema(block) {
304
343
  const out = {};
@@ -0,0 +1,37 @@
1
+ import { type ReactNode, type ReactElement } from 'react';
2
+ import { type NodeViewProps } from '@tiptap/react';
3
+ import { type BlockSetting } from './BlockSettingsMenu.js';
4
+ /**
5
+ * Shared chrome for the width-aware content blocks (labelled blocks + Pros &
6
+ * cons). Every one of them renders the same two-layer shell — a full-width
7
+ * outer anchor (`cssClass`, stable so the gear doesn't jump on a width toggle)
8
+ * hosting the in-block **gear menu**, with the block's own content as
9
+ * `children`. This component owns the parts that are byte-identical across
10
+ * those blocks so they can't drift:
11
+ *
12
+ * - read the `width` attr → `data-width`,
13
+ * - build the **Width** setting (contained / full) for the gear,
14
+ * - render the `NodeViewWrapper` + the hover-revealed `BlockSettingsMenu`
15
+ * (editor-only), deriving the hover selector from `cssClass`.
16
+ *
17
+ * The caller supplies only what actually varies: the anchor `cssClass`, the
18
+ * gear's accessible `settingsLabel`, the inner content, and (optionally) any
19
+ * block-specific settings to show alongside Width.
20
+ *
21
+ * `data-type` always mirrors the node's own type name — every block that uses
22
+ * this shell wants exactly that.
23
+ */
24
+ export interface BlockGearShellProps {
25
+ node: NodeViewProps['node'];
26
+ editor: NodeViewProps['editor'];
27
+ updateAttributes: NodeViewProps['updateAttributes'];
28
+ /** Outer anchor class — also drives the gear's hover-reveal selector. */
29
+ cssClass: string;
30
+ /** Accessible label for the gear trigger, e.g. `"Summary settings"`. */
31
+ settingsLabel: string;
32
+ /** Block-specific settings shown after Width (none today; future-proofing). */
33
+ extraSettings?: BlockSetting[];
34
+ children: ReactNode;
35
+ }
36
+ export declare function BlockGearShell({ node, editor, updateAttributes, cssClass, settingsLabel, extraSettings, children, }: BlockGearShellProps): ReactElement;
37
+ //# sourceMappingURL=BlockGearShell.d.ts.map
@@ -0,0 +1,21 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { NodeViewWrapper } from '@tiptap/react';
3
+ import { BlockSettingsMenu } from './BlockSettingsMenu.js';
4
+ export function BlockGearShell({ node, editor, updateAttributes, cssClass, settingsLabel, extraSettings, children, }) {
5
+ const width = node.attrs['width'] === 'full' ? 'full' : 'contained';
6
+ const settings = [
7
+ {
8
+ kind: 'select',
9
+ key: 'width',
10
+ label: 'Width',
11
+ value: width,
12
+ options: [
13
+ { value: 'contained', label: 'Contained' },
14
+ { value: 'full', label: 'Full width' },
15
+ ],
16
+ onChange: (w) => updateAttributes({ width: w }),
17
+ },
18
+ ...(extraSettings ?? []),
19
+ ];
20
+ return (_jsxs(NodeViewWrapper, { "data-type": node.type.name, "data-width": width, className: cssClass + ' relative', children: [editor.isEditable && (_jsx(BlockSettingsMenu, { settings: settings, label: settingsLabel, hoverClass: '[.' + cssClass + ':hover_&]:opacity-100' })), children] }));
21
+ }
@@ -7,12 +7,10 @@ import { type NodeViewProps } from '@tiptap/react';
7
7
  * bespoke component each; the per-block `label` + `cssClass` ride the node's
8
8
  * `addOptions()` (see `labeledBlock()` in `extensions/contentBlocks.ts`).
9
9
  *
10
- * Mirrors the FAQ/Alert layout: a full-width outer anchor (`cssClass`, stays
11
- * put so the gear doesn't move on a width toggle) wrapping an inner
12
- * `.pilotiq-block-content` that carries the max-width / centering. The only
13
- * setting today is **Width** (contained vs full) via the in-block gear menu;
14
- * the `width` attr drives `data-width`, which the consumer's CSS reads (shared
15
- * with the read-side `render.ts`).
10
+ * The two-layer shell (full-width anchor + gear + the inner
11
+ * `.pilotiq-block-content` that carries the max-width / centering) lives in the
12
+ * shared `BlockGearShell`, alongside the Width gear setting. This component
13
+ * only supplies the label + body content hole.
16
14
  */
17
15
  export declare function LabeledBlockNodeView({ node, updateAttributes, editor, extension }: NodeViewProps): ReactElement;
18
16
  //# sourceMappingURL=LabeledBlockNodeView.d.ts.map
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { NodeViewWrapper, NodeViewContent } from '@tiptap/react';
3
- import { BlockSettingsMenu } from './BlockSettingsMenu.js';
2
+ import { NodeViewContent } from '@tiptap/react';
3
+ import { BlockGearShell } from './BlockGearShell.js';
4
4
  /**
5
5
  * Shared React NodeView for the simple **labelled** content blocks — the ones
6
6
  * that are just a non-editable label above a `block+` body (Key takeaways,
@@ -8,28 +8,12 @@ import { BlockSettingsMenu } from './BlockSettingsMenu.js';
8
8
  * bespoke component each; the per-block `label` + `cssClass` ride the node's
9
9
  * `addOptions()` (see `labeledBlock()` in `extensions/contentBlocks.ts`).
10
10
  *
11
- * Mirrors the FAQ/Alert layout: a full-width outer anchor (`cssClass`, stays
12
- * put so the gear doesn't move on a width toggle) wrapping an inner
13
- * `.pilotiq-block-content` that carries the max-width / centering. The only
14
- * setting today is **Width** (contained vs full) via the in-block gear menu;
15
- * the `width` attr drives `data-width`, which the consumer's CSS reads (shared
16
- * with the read-side `render.ts`).
11
+ * The two-layer shell (full-width anchor + gear + the inner
12
+ * `.pilotiq-block-content` that carries the max-width / centering) lives in the
13
+ * shared `BlockGearShell`, alongside the Width gear setting. This component
14
+ * only supplies the label + body content hole.
17
15
  */
18
16
  export function LabeledBlockNodeView({ node, updateAttributes, editor, extension }) {
19
17
  const { label, cssClass } = extension.options;
20
- const width = node.attrs['width'] === 'full' ? 'full' : 'contained';
21
- const settings = [
22
- {
23
- kind: 'select',
24
- key: 'width',
25
- label: 'Width',
26
- value: width,
27
- options: [
28
- { value: 'contained', label: 'Contained' },
29
- { value: 'full', label: 'Full width' },
30
- ],
31
- onChange: (w) => updateAttributes({ width: w }),
32
- },
33
- ];
34
- return (_jsxs(NodeViewWrapper, { "data-type": node.type.name, "data-width": width, className: cssClass + ' relative', children: [editor.isEditable && (_jsx(BlockSettingsMenu, { settings: settings, label: label + ' settings', hoverClass: '[.' + cssClass + ':hover_&]:opacity-100' })), _jsxs("div", { className: "pilotiq-block-content", children: [_jsx("div", { className: "pilotiq-block-label", contentEditable: false, children: label }), _jsx(NodeViewContent, { className: "pilotiq-block-body" })] })] }));
18
+ return (_jsx(BlockGearShell, { node: node, editor: editor, updateAttributes: updateAttributes, cssClass: cssClass, settingsLabel: label + ' settings', children: _jsxs("div", { className: "pilotiq-block-content", children: [_jsx("div", { className: "pilotiq-block-label", contentEditable: false, children: label }), _jsx(NodeViewContent, { className: "pilotiq-block-body" })] }) }));
35
19
  }
@@ -4,11 +4,10 @@ import { type NodeViewProps } from '@tiptap/react';
4
4
  * React NodeView for the `prosCons` container — hosts the in-block **gear menu**
5
5
  * (Width) at the top-end corner. The two `prosColumn` / `consColumn` children
6
6
  * keep their plain `renderHTML` (label + body); only the container needs React,
7
- * for the gear. Two layers like FAQ/Alert: a full-width outer anchor
8
- * (`.pilotiq-pros-cons`, stable so the gear doesn't move on a width toggle)
9
- * wrapping the inner `.pilotiq-pros-cons-content`, which carries the two-column
10
- * grid + max-width / centering. The `width` attr drives `data-width` (read by
11
- * the consumer CSS, shared with the read-side renderer).
7
+ * for the gear. The two-layer shell (full-width anchor + gear) lives in the
8
+ * shared `BlockGearShell`; this component only supplies the inner
9
+ * `.pilotiq-pros-cons-content`, which carries the two-column grid + max-width /
10
+ * centering.
12
11
  */
13
12
  export declare function ProsConsNodeView({ node, updateAttributes, editor }: NodeViewProps): ReactElement;
14
13
  //# sourceMappingURL=ProsConsNodeView.d.ts.map
@@ -1,30 +1,15 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { NodeViewWrapper, NodeViewContent } from '@tiptap/react';
3
- import { BlockSettingsMenu } from './BlockSettingsMenu.js';
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { NodeViewContent } from '@tiptap/react';
3
+ import { BlockGearShell } from './BlockGearShell.js';
4
4
  /**
5
5
  * React NodeView for the `prosCons` container — hosts the in-block **gear menu**
6
6
  * (Width) at the top-end corner. The two `prosColumn` / `consColumn` children
7
7
  * keep their plain `renderHTML` (label + body); only the container needs React,
8
- * for the gear. Two layers like FAQ/Alert: a full-width outer anchor
9
- * (`.pilotiq-pros-cons`, stable so the gear doesn't move on a width toggle)
10
- * wrapping the inner `.pilotiq-pros-cons-content`, which carries the two-column
11
- * grid + max-width / centering. The `width` attr drives `data-width` (read by
12
- * the consumer CSS, shared with the read-side renderer).
8
+ * for the gear. The two-layer shell (full-width anchor + gear) lives in the
9
+ * shared `BlockGearShell`; this component only supplies the inner
10
+ * `.pilotiq-pros-cons-content`, which carries the two-column grid + max-width /
11
+ * centering.
13
12
  */
14
13
  export function ProsConsNodeView({ node, updateAttributes, editor }) {
15
- const width = node.attrs['width'] === 'full' ? 'full' : 'contained';
16
- const settings = [
17
- {
18
- kind: 'select',
19
- key: 'width',
20
- label: 'Width',
21
- value: width,
22
- options: [
23
- { value: 'contained', label: 'Contained' },
24
- { value: 'full', label: 'Full width' },
25
- ],
26
- onChange: (w) => updateAttributes({ width: w }),
27
- },
28
- ];
29
- return (_jsxs(NodeViewWrapper, { "data-type": "prosCons", "data-width": width, className: "pilotiq-pros-cons relative", children: [editor.isEditable && (_jsx(BlockSettingsMenu, { settings: settings, label: "Pros & cons settings", hoverClass: "[.pilotiq-pros-cons:hover_&]:opacity-100" })), _jsx(NodeViewContent, { className: "pilotiq-pros-cons-content" })] }));
14
+ return (_jsx(BlockGearShell, { node: node, editor: editor, updateAttributes: updateAttributes, cssClass: "pilotiq-pros-cons", settingsLabel: "Pros & cons settings", children: _jsx(NodeViewContent, { className: "pilotiq-pros-cons-content" }) }));
30
15
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pilotiq/tiptap",
3
- "version": "3.15.0",
3
+ "version": "3.15.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": {