@pilotiq/tiptap 3.15.0 → 3.16.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 +21 -0
- package/dist/extensions/SlashCommandExtension.js +40 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +6 -0
- package/dist/react/BlockGearShell.d.ts +37 -0
- package/dist/react/BlockGearShell.js +21 -0
- package/dist/react/LabeledBlockNodeView.d.ts +4 -6
- package/dist/react/LabeledBlockNodeView.js +7 -23
- package/dist/react/ProsConsNodeView.d.ts +4 -5
- package/dist/react/ProsConsNodeView.js +8 -23
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# @pilotiq/tiptap
|
|
2
2
|
|
|
3
|
+
## 3.16.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 58e9461: Export the default content-block node specs (`contentBlockNodes`, plus `Faq` / `FaqItem` / `FaqQuestion` / `FaqAnswer` / `Alert` / `AlertTitle` / `AlertBody` / `Summary` / `KeyTakeaways` / `ProsCons` / `ProsColumn` / `ConsColumn` / `ContentBlockKeymap` and the `AlertType` helpers) from the package entry. `contentBlockNodes` is the exact array `TiptapEditor` registers, so consumers can build a headless editor whose schema matches the live one — e.g. to parse content-block HTML or drive the surgical-op planners (`planInsertBlockBefore` & co.) in a test — without mounting React. Additive; no behavior change.
|
|
8
|
+
|
|
9
|
+
## 3.15.1
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 2bc1e54: Slash menu now ranks results by relevance instead of definition order.
|
|
14
|
+
|
|
15
|
+
A query that matches an entry's **label** (exactly, by prefix, or by word) now
|
|
16
|
+
ranks above an entry that only mentions the word in its `searchKey`. Previously
|
|
17
|
+
the menu was a plain substring filter that preserved definition order, so typing
|
|
18
|
+
`/summary` surfaced **Collapsible block** first — its `searchKey` lists
|
|
19
|
+
"summary" and it's defined before the Summary block — and pressing Enter
|
|
20
|
+
inserted the wrong block. The matched set is unchanged (every entry that matched
|
|
21
|
+
before still matches); only the ordering improves. Ties keep their original menu
|
|
22
|
+
order.
|
|
23
|
+
|
|
3
24
|
## 3.15.0
|
|
4
25
|
|
|
5
26
|
### 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
|
-
|
|
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 = {};
|
package/dist/index.d.ts
CHANGED
|
@@ -11,4 +11,5 @@ export { useAiSuggestionBridge } from './react/useAiSuggestionBridge.js';
|
|
|
11
11
|
export { AiInlineDiffExtension, aiInlineDiffPluginKey, getAiInlineDiffState, type AiInlineDiffExtensionOptions, } from './extensions/AiInlineDiffExtension.js';
|
|
12
12
|
export { planReplaceBlock, planInsertBlockBefore, planDeleteBlock, planUpdateBlockMark, summarizeBlockStructure, type BlockMarkRange, type TransactionModifier, } from './surgicalOps.js';
|
|
13
13
|
export { renderRichTextToHtml, isRichTextValue, type RenderRichTextOptions, type TiptapNode, type TiptapMark, } from './render.js';
|
|
14
|
+
export { contentBlockNodes, Faq, FaqItem, FaqQuestion, FaqAnswer, Alert, AlertTitle, AlertBody, Summary, KeyTakeaways, ProsCons, ProsColumn, ConsColumn, ContentBlockKeymap, ALERT_VARIANTS, ALERT_VARIANT_LABEL, coerceAlertType, type AlertType, } from './extensions/contentBlocks.js';
|
|
14
15
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -11,3 +11,9 @@ export { useAiSuggestionBridge } from './react/useAiSuggestionBridge.js';
|
|
|
11
11
|
export { AiInlineDiffExtension, aiInlineDiffPluginKey, getAiInlineDiffState, } from './extensions/AiInlineDiffExtension.js';
|
|
12
12
|
export { planReplaceBlock, planInsertBlockBefore, planDeleteBlock, planUpdateBlockMark, summarizeBlockStructure, } from './surgicalOps.js';
|
|
13
13
|
export { renderRichTextToHtml, isRichTextValue, } from './render.js';
|
|
14
|
+
// Default content-block node specs (FAQ / Alert / Summary / Key takeaways /
|
|
15
|
+
// Pros & cons). `contentBlockNodes` is the exact array `TiptapEditor` registers,
|
|
16
|
+
// so a consumer can build a headless editor whose schema matches the live
|
|
17
|
+
// editor — e.g. to parse the content-block HTML or drive the surgical-op
|
|
18
|
+
// planners (`planInsertBlockBefore` & co.) in a test, without mounting React.
|
|
19
|
+
export { contentBlockNodes, Faq, FaqItem, FaqQuestion, FaqAnswer, Alert, AlertTitle, AlertBody, Summary, KeyTakeaways, ProsCons, ProsColumn, ConsColumn, ContentBlockKeymap, ALERT_VARIANTS, ALERT_VARIANT_LABEL, coerceAlertType, } from './extensions/contentBlocks.js';
|
|
@@ -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
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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 {
|
|
3
|
-
import {
|
|
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
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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
|
-
|
|
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.
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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.
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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
|
-
|
|
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
|
}
|