@pilotiq/tiptap 3.13.0 → 3.15.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 +56 -0
- package/dist/extensions/SlashCommandExtension.js +13 -18
- package/dist/extensions/alertVariants.d.ts +44 -0
- package/dist/extensions/alertVariants.js +132 -0
- package/dist/extensions/contentBlocks.d.ts +19 -4
- package/dist/extensions/contentBlocks.js +334 -40
- package/dist/react/AlertNodeView.d.ts +4 -0
- package/dist/react/AlertNodeView.js +137 -0
- package/dist/react/BlockSettingsMenu.d.ts +47 -0
- package/dist/react/BlockSettingsMenu.js +16 -0
- package/dist/react/FaqItemNodeView.d.ts +17 -0
- package/dist/react/FaqItemNodeView.js +20 -0
- package/dist/react/FaqNodeView.d.ts +11 -0
- package/dist/react/FaqNodeView.js +27 -0
- package/dist/react/LabeledBlockNodeView.d.ts +18 -0
- package/dist/react/LabeledBlockNodeView.js +35 -0
- package/dist/react/MarkdownEditor.js +19 -0
- package/dist/react/ProsConsNodeView.d.ts +14 -0
- package/dist/react/ProsConsNodeView.js +30 -0
- package/dist/render.js +86 -21
- package/package.json +4 -2
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { type ReactElement, type ReactNode } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Reusable in-block **settings** menu — a gear trigger outset in the inline-end
|
|
4
|
+
* gutter (mirroring the drag handle on the start side), opening a nested menu
|
|
5
|
+
* where each block setting is a submenu. Blocks with multiple variations (Alert
|
|
6
|
+
* = width + type + icon + color; FAQ = width) all surface their controls through
|
|
7
|
+
* one consistent entry point instead of a scattered control cluster.
|
|
8
|
+
*
|
|
9
|
+
* Two setting kinds:
|
|
10
|
+
* - `select` — a radio submenu of mutually-exclusive options (Width, Type). The
|
|
11
|
+
* active option's label rides the parent row as a hint; picking keeps the menu
|
|
12
|
+
* open so the change previews live.
|
|
13
|
+
* - `custom` — a submenu whose body is caller-supplied JSX (icon grid, color
|
|
14
|
+
* swatches). The caller owns the interactions; plain buttons inside don't
|
|
15
|
+
* auto-close the menu.
|
|
16
|
+
*
|
|
17
|
+
* The caller owns every attr write + the matching `data-*` / read-side CSS.
|
|
18
|
+
*/
|
|
19
|
+
export type BlockWidth = 'contained' | 'full';
|
|
20
|
+
export type BlockSettingOption = {
|
|
21
|
+
value: string;
|
|
22
|
+
label: string;
|
|
23
|
+
icon?: ReactNode;
|
|
24
|
+
};
|
|
25
|
+
export type BlockSetting = {
|
|
26
|
+
kind: 'select';
|
|
27
|
+
key: string;
|
|
28
|
+
label: string;
|
|
29
|
+
value: string;
|
|
30
|
+
options: BlockSettingOption[];
|
|
31
|
+
onChange: (value: string) => void;
|
|
32
|
+
} | {
|
|
33
|
+
kind: 'custom';
|
|
34
|
+
key: string;
|
|
35
|
+
label: string;
|
|
36
|
+
/** Short hint shown on the parent row (e.g. the current icon / color). */
|
|
37
|
+
hint?: ReactNode;
|
|
38
|
+
/** Submenu body — caller-supplied JSX. */
|
|
39
|
+
content: ReactNode;
|
|
40
|
+
};
|
|
41
|
+
export declare function BlockSettingsMenu({ settings, hoverClass, label, triggerClass, }: {
|
|
42
|
+
settings: BlockSetting[];
|
|
43
|
+
hoverClass?: string;
|
|
44
|
+
label?: string;
|
|
45
|
+
triggerClass?: string;
|
|
46
|
+
}): ReactElement;
|
|
47
|
+
//# sourceMappingURL=BlockSettingsMenu.d.ts.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Menu } from '@base-ui/react/menu';
|
|
3
|
+
const gear = (_jsxs("svg", { viewBox: "0 0 24 24", className: "size-3.5", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, children: [_jsx("circle", { cx: "12", cy: "12", r: "3" }), _jsx("path", { d: "M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z" })] }));
|
|
4
|
+
const chevronRight = (_jsx("svg", { viewBox: "0 0 24 24", className: "ms-auto size-3.5 text-muted-foreground", fill: "none", stroke: "currentColor", strokeWidth: 2, "aria-hidden": true, children: _jsx("path", { d: "m9 18 6-6-6-6" }) }));
|
|
5
|
+
const check = (_jsx("svg", { viewBox: "0 0 24 24", className: "size-3.5", fill: "none", stroke: "currentColor", strokeWidth: 2.5, "aria-hidden": true, children: _jsx("path", { d: "M20 6 9 17l-5-5" }) }));
|
|
6
|
+
const POPUP = 'min-w-44 rounded-md border bg-popover p-1 text-popover-foreground shadow-md outline-hidden';
|
|
7
|
+
const ROW = 'flex w-full cursor-default items-center gap-2 rounded px-2 py-1.5 text-sm outline-hidden ' +
|
|
8
|
+
'data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground';
|
|
9
|
+
function activeLabel(s) {
|
|
10
|
+
return s.options.find((o) => o.value === s.value)?.label ?? '';
|
|
11
|
+
}
|
|
12
|
+
export function BlockSettingsMenu({ settings, hoverClass = '[.pilotiq-faq:hover_&]:opacity-100', label = 'Block settings', triggerClass = 'top-0 -end-[30px]', }) {
|
|
13
|
+
return (_jsxs(Menu.Root, { children: [_jsx(Menu.Trigger, { render: _jsx("button", { type: "button", contentEditable: false, "aria-label": label, className: 'absolute z-10 flex items-center justify-center rounded border bg-background p-1 text-muted-foreground shadow-sm ' +
|
|
14
|
+
'opacity-0 transition-opacity hover:text-foreground focus-visible:opacity-100 data-[popup-open]:opacity-100 ' +
|
|
15
|
+
triggerClass + ' ' + hoverClass, children: gear }) }), _jsx(Menu.Portal, { children: _jsx(Menu.Positioner, { side: "bottom", align: "end", sideOffset: 4, className: "isolate z-50", children: _jsx(Menu.Popup, { className: POPUP, children: settings.map((s) => (_jsxs(Menu.SubmenuRoot, { children: [_jsxs(Menu.SubmenuTrigger, { className: ROW, children: [_jsx("span", { children: s.label }), _jsx("span", { className: "ms-auto flex items-center gap-1 text-xs text-muted-foreground", children: s.kind === 'select' ? activeLabel(s) : s.hint }), chevronRight] }), _jsx(Menu.Portal, { children: _jsx(Menu.Positioner, { side: "inline-end", align: "start", sideOffset: 2, className: "isolate z-50", children: _jsx(Menu.Popup, { className: POPUP, children: s.kind === 'select' ? (_jsx(Menu.RadioGroup, { value: s.value, onValueChange: (v) => s.onChange(String(v)), children: s.options.map((o) => (_jsxs(Menu.RadioItem, { value: o.value, className: ROW, children: [o.icon && _jsx("span", { className: "flex size-4 items-center justify-center", children: o.icon }), _jsx("span", { children: o.label }), _jsx(Menu.RadioItemIndicator, { className: "ms-auto flex", children: check })] }, o.value))) })) : (s.content) }) }) })] }, s.key))) }) }) })] }));
|
|
16
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type ReactElement } from 'react';
|
|
2
|
+
import { type NodeViewProps } from '@tiptap/react';
|
|
3
|
+
/**
|
|
4
|
+
* React NodeView for a `faqItem` — one row of the FAQ accordion. The question
|
|
5
|
+
* is the always-visible trigger; the answer collapses below it. The chevron (on
|
|
6
|
+
* the right, shadcn-style) toggles the item's `open` attr — the same attr the
|
|
7
|
+
* read-side `<details>` accordion reads, so the editor state is what publishes.
|
|
8
|
+
* The question stays editable on click; only the chevron toggles.
|
|
9
|
+
*
|
|
10
|
+
* One `<NodeViewContent>` holds both child nodes (`faqQuestion` + `faqAnswer`).
|
|
11
|
+
* Layout + the collapse (hiding `.pilotiq-faq-answer` when `data-open="false"`)
|
|
12
|
+
* live in the consumer's `.pilotiq-faq*` CSS — shared with the read-side — so
|
|
13
|
+
* the editable answer stays mounted (round-trips through FormData on save) and
|
|
14
|
+
* the editor matches the published look.
|
|
15
|
+
*/
|
|
16
|
+
export declare function FaqItemNodeView({ node, updateAttributes, editor }: NodeViewProps): ReactElement;
|
|
17
|
+
//# sourceMappingURL=FaqItemNodeView.d.ts.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { NodeViewWrapper, NodeViewContent } from '@tiptap/react';
|
|
3
|
+
/**
|
|
4
|
+
* React NodeView for a `faqItem` — one row of the FAQ accordion. The question
|
|
5
|
+
* is the always-visible trigger; the answer collapses below it. The chevron (on
|
|
6
|
+
* the right, shadcn-style) toggles the item's `open` attr — the same attr the
|
|
7
|
+
* read-side `<details>` accordion reads, so the editor state is what publishes.
|
|
8
|
+
* The question stays editable on click; only the chevron toggles.
|
|
9
|
+
*
|
|
10
|
+
* One `<NodeViewContent>` holds both child nodes (`faqQuestion` + `faqAnswer`).
|
|
11
|
+
* Layout + the collapse (hiding `.pilotiq-faq-answer` when `data-open="false"`)
|
|
12
|
+
* live in the consumer's `.pilotiq-faq*` CSS — shared with the read-side — so
|
|
13
|
+
* the editable answer stays mounted (round-trips through FormData on save) and
|
|
14
|
+
* the editor matches the published look.
|
|
15
|
+
*/
|
|
16
|
+
export function FaqItemNodeView({ node, updateAttributes, editor }) {
|
|
17
|
+
const open = node.attrs['open'] !== false;
|
|
18
|
+
const editable = editor.isEditable;
|
|
19
|
+
return (_jsxs(NodeViewWrapper, { "data-type": "faqItem", "data-open": open ? 'true' : 'false', className: "pilotiq-faq-item relative", children: [_jsx("button", { type: "button", contentEditable: false, "aria-label": open ? 'Collapse answer' : 'Expand answer', "aria-expanded": open, disabled: !editable, onClick: () => updateAttributes({ open: !open }), className: "absolute start-0 top-2.5 flex size-5 items-center justify-center text-muted-foreground/60 transition-transform hover:text-foreground", style: { transform: open ? 'rotate(180deg)' : 'rotate(0deg)' }, children: _jsx("svg", { viewBox: "0 0 24 24", className: "size-4", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, children: _jsx("path", { d: "m6 9 6 6 6-6" }) }) }), _jsx(NodeViewContent, {})] }));
|
|
20
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type ReactElement } from 'react';
|
|
2
|
+
import { type NodeViewProps } from '@tiptap/react';
|
|
3
|
+
/**
|
|
4
|
+
* React NodeView for the `faq` container — hosts the in-block **gear menu** at
|
|
5
|
+
* the top-end corner (chevrons live at the start, so the end is free). For now
|
|
6
|
+
* its only setting is **Width** (contained vs full); the `width` attr drives
|
|
7
|
+
* `data-width`, which the consumer's `.pilotiq-faq[data-width="full"]` CSS reads
|
|
8
|
+
* (shared with the read-side). The faqItems render through `<NodeViewContent>`.
|
|
9
|
+
*/
|
|
10
|
+
export declare function FaqNodeView({ node, updateAttributes, editor }: NodeViewProps): ReactElement;
|
|
11
|
+
//# sourceMappingURL=FaqNodeView.d.ts.map
|
|
@@ -0,0 +1,27 @@
|
|
|
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';
|
|
4
|
+
/**
|
|
5
|
+
* React NodeView for the `faq` container — hosts the in-block **gear menu** at
|
|
6
|
+
* the top-end corner (chevrons live at the start, so the end is free). For now
|
|
7
|
+
* its only setting is **Width** (contained vs full); the `width` attr drives
|
|
8
|
+
* `data-width`, which the consumer's `.pilotiq-faq[data-width="full"]` CSS reads
|
|
9
|
+
* (shared with the read-side). The faqItems render through `<NodeViewContent>`.
|
|
10
|
+
*/
|
|
11
|
+
export function FaqNodeView({ node, updateAttributes, editor }) {
|
|
12
|
+
const width = node.attrs['width'] === 'full' ? 'full' : 'contained';
|
|
13
|
+
const settings = [
|
|
14
|
+
{
|
|
15
|
+
kind: 'select',
|
|
16
|
+
key: 'width',
|
|
17
|
+
label: 'Width',
|
|
18
|
+
value: width,
|
|
19
|
+
options: [
|
|
20
|
+
{ value: 'contained', label: 'Contained' },
|
|
21
|
+
{ value: 'full', label: 'Full width' },
|
|
22
|
+
],
|
|
23
|
+
onChange: (w) => updateAttributes({ width: w }),
|
|
24
|
+
},
|
|
25
|
+
];
|
|
26
|
+
return (_jsxs(NodeViewWrapper, { "data-type": "faq", "data-width": width, className: "pilotiq-faq relative", children: [editor.isEditable && _jsx(BlockSettingsMenu, { settings: settings, label: "FAQ settings" }), _jsx(NodeViewContent, { className: "pilotiq-faq-content" })] }));
|
|
27
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type ReactElement } from 'react';
|
|
2
|
+
import { type NodeViewProps } from '@tiptap/react';
|
|
3
|
+
/**
|
|
4
|
+
* Shared React NodeView for the simple **labelled** content blocks — the ones
|
|
5
|
+
* that are just a non-editable label above a `block+` body (Key takeaways,
|
|
6
|
+
* Summary). They rhyme structurally, so they share ONE NodeView instead of a
|
|
7
|
+
* bespoke component each; the per-block `label` + `cssClass` ride the node's
|
|
8
|
+
* `addOptions()` (see `labeledBlock()` in `extensions/contentBlocks.ts`).
|
|
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`).
|
|
16
|
+
*/
|
|
17
|
+
export declare function LabeledBlockNodeView({ node, updateAttributes, editor, extension }: NodeViewProps): ReactElement;
|
|
18
|
+
//# sourceMappingURL=LabeledBlockNodeView.d.ts.map
|
|
@@ -0,0 +1,35 @@
|
|
|
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';
|
|
4
|
+
/**
|
|
5
|
+
* Shared React NodeView for the simple **labelled** content blocks — the ones
|
|
6
|
+
* that are just a non-editable label above a `block+` body (Key takeaways,
|
|
7
|
+
* Summary). They rhyme structurally, so they share ONE NodeView instead of a
|
|
8
|
+
* bespoke component each; the per-block `label` + `cssClass` ride the node's
|
|
9
|
+
* `addOptions()` (see `labeledBlock()` in `extensions/contentBlocks.ts`).
|
|
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`).
|
|
17
|
+
*/
|
|
18
|
+
export function LabeledBlockNodeView({ node, updateAttributes, editor, extension }) {
|
|
19
|
+
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" })] })] }));
|
|
35
|
+
}
|
|
@@ -15,6 +15,7 @@ import { useCollabRoom, getCollabExtensions, useToast, } from '@pilotiq/pilotiq/
|
|
|
15
15
|
import { useCollabSeed } from '@rudderjs/sync/react';
|
|
16
16
|
import { AiSuggestionExtension } from '../extensions/AiSuggestionExtension.js';
|
|
17
17
|
import { AiInlineDiffExtension } from '../extensions/AiInlineDiffExtension.js';
|
|
18
|
+
import { Alert, AlertTitle, AlertBody, ContentBlockKeymap } from '../extensions/contentBlocks.js';
|
|
18
19
|
import { useAiSuggestionBridge } from './useAiSuggestionBridge.js';
|
|
19
20
|
import { useAiInlineDiff, useIsAiInlineDiffActive, readAiDiffViewMarker } from './useAiInlineDiff.js';
|
|
20
21
|
import { AiSuggestionBanner } from './AiSuggestionBanner.js';
|
|
@@ -39,6 +40,7 @@ const SvgIcons = {
|
|
|
39
40
|
orderedList: (_jsxs("svg", { ...ICON_PROPS, children: [_jsx("line", { x1: "10", y1: "6", x2: "21", y2: "6" }), _jsx("line", { x1: "10", y1: "12", x2: "21", y2: "12" }), _jsx("line", { x1: "10", y1: "18", x2: "21", y2: "18" }), _jsx("path", { d: "M4 6h1v4" }), _jsx("path", { d: "M4 10h2" }), _jsx("path", { d: "M6 18H4c0-1 2-2 2-3s-1-1.5-2-1" })] })),
|
|
40
41
|
blockquote: (_jsxs("svg", { ...ICON_PROPS, children: [_jsx("path", { d: "M3 21c3 0 7-1 7-8V5a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h3" }), _jsx("path", { d: "M15 21c3 0 7-1 7-8V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h3" })] })),
|
|
41
42
|
codeBlock: (_jsxs("svg", { ...ICON_PROPS, children: [_jsx("polyline", { points: "16 18 22 12 16 6" }), _jsx("polyline", { points: "8 6 2 12 8 18" })] })),
|
|
43
|
+
alert: (_jsxs("svg", { ...ICON_PROPS, children: [_jsx("path", { d: "m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z" }), _jsx("path", { d: "M12 9v4" }), _jsx("path", { d: "M12 17h.01" })] })),
|
|
42
44
|
attachFiles: (_jsx("svg", { ...ICON_PROPS, children: _jsx("path", { d: "m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48" }) })),
|
|
43
45
|
pencil: (_jsxs("svg", { ...ICON_PROPS, children: [_jsx("path", { d: "M12 20h9" }), _jsx("path", { d: "M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4Z" })] })),
|
|
44
46
|
source: (_jsxs("svg", { ...ICON_PROPS, children: [_jsx("path", { d: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" }), _jsx("polyline", { points: "14 2 14 8 20 8" }), _jsx("line", { x1: "9", y1: "13", x2: "15", y2: "13" }), _jsx("line", { x1: "9", y1: "17", x2: "15", y2: "17" })] })),
|
|
@@ -140,6 +142,12 @@ export function MarkdownEditor({ name, fragmentKey, defaultValue, placeholder, d
|
|
|
140
142
|
// for deleted text. Host's `<AiSuggestionBanner>` drives Accept /
|
|
141
143
|
// Reject via the extension's commands.
|
|
142
144
|
AiInlineDiffExtension,
|
|
145
|
+
// Alert content block — round-trips to `:::alert{type=…} Title` via the
|
|
146
|
+
// node's `markdown` storage spec; renders the same shadcn NodeView.
|
|
147
|
+
Alert,
|
|
148
|
+
AlertTitle,
|
|
149
|
+
AlertBody,
|
|
150
|
+
ContentBlockKeymap,
|
|
143
151
|
...collabExtensions,
|
|
144
152
|
],
|
|
145
153
|
// Collab takes ownership of the document — passing `content` would
|
|
@@ -366,6 +374,16 @@ export function MarkdownEditor({ name, fragmentKey, defaultValue, placeholder, d
|
|
|
366
374
|
case 'codeBlock':
|
|
367
375
|
c.toggleCodeBlock().run();
|
|
368
376
|
break;
|
|
377
|
+
case 'alert':
|
|
378
|
+
c.insertContent({
|
|
379
|
+
type: 'alert',
|
|
380
|
+
attrs: { type: 'info' },
|
|
381
|
+
content: [
|
|
382
|
+
{ type: 'alertTitle', content: [{ type: 'text', text: 'Info' }] },
|
|
383
|
+
{ type: 'alertBody', content: [{ type: 'paragraph' }] },
|
|
384
|
+
],
|
|
385
|
+
}).run();
|
|
386
|
+
break;
|
|
369
387
|
case 'attachFiles':
|
|
370
388
|
onAttachClick();
|
|
371
389
|
break;
|
|
@@ -398,6 +416,7 @@ export function MarkdownEditor({ name, fragmentKey, defaultValue, placeholder, d
|
|
|
398
416
|
orderedList: 'Numbered list',
|
|
399
417
|
blockquote: 'Quote',
|
|
400
418
|
codeBlock: 'Code block',
|
|
419
|
+
alert: 'Alert',
|
|
401
420
|
attachFiles: 'Attach file',
|
|
402
421
|
};
|
|
403
422
|
const wrapperStyle = {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type ReactElement } from 'react';
|
|
2
|
+
import { type NodeViewProps } from '@tiptap/react';
|
|
3
|
+
/**
|
|
4
|
+
* React NodeView for the `prosCons` container — hosts the in-block **gear menu**
|
|
5
|
+
* (Width) at the top-end corner. The two `prosColumn` / `consColumn` children
|
|
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).
|
|
12
|
+
*/
|
|
13
|
+
export declare function ProsConsNodeView({ node, updateAttributes, editor }: NodeViewProps): ReactElement;
|
|
14
|
+
//# sourceMappingURL=ProsConsNodeView.d.ts.map
|
|
@@ -0,0 +1,30 @@
|
|
|
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';
|
|
4
|
+
/**
|
|
5
|
+
* React NodeView for the `prosCons` container — hosts the in-block **gear menu**
|
|
6
|
+
* (Width) at the top-end corner. The two `prosColumn` / `consColumn` children
|
|
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).
|
|
13
|
+
*/
|
|
14
|
+
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" })] }));
|
|
30
|
+
}
|
package/dist/render.js
CHANGED
|
@@ -32,6 +32,9 @@
|
|
|
32
32
|
* custom blocks — any other type renders to `<div data-type="..."
|
|
33
33
|
* data-attrs="...">` so consumers can replay or style by data-type.
|
|
34
34
|
*/
|
|
35
|
+
// Pure variant primitives (no Tiptap runtime) — keeps render.ts server-safe
|
|
36
|
+
// while sharing the alert icon/label source of truth with the editor NodeView.
|
|
37
|
+
import { coerceAlertType, ALERT_VARIANT_LABEL, buildAlertIconSvg } from './extensions/alertVariants.js';
|
|
35
38
|
/**
|
|
36
39
|
* Render Tiptap content to HTML.
|
|
37
40
|
*
|
|
@@ -142,14 +145,14 @@ function renderNode(node, opts) {
|
|
|
142
145
|
case 'detailsContent': return renderChildren(n, opts);
|
|
143
146
|
case 'grid': return renderGrid(n, opts);
|
|
144
147
|
case 'gridColumn': return wrap('div', n, opts);
|
|
145
|
-
case 'keyTakeaways': return labeledBlockHtml('pilotiq-key-takeaways', 'Key takeaways', n, opts);
|
|
146
|
-
case 'summary': return labeledBlockHtml('pilotiq-summary', 'Summary', n, opts);
|
|
147
|
-
case 'faq': return
|
|
148
|
-
case 'faqItem': return
|
|
149
|
-
case 'faqQuestion': return `<
|
|
150
|
-
case 'faqAnswer': return `<div class="pilotiq-faq-answer"
|
|
148
|
+
case 'keyTakeaways': return labeledBlockHtml('pilotiq-key-takeaways', 'Key takeaways', n, opts, true);
|
|
149
|
+
case 'summary': return labeledBlockHtml('pilotiq-summary', 'Summary', n, opts, true);
|
|
150
|
+
case 'faq': return renderFaqNode(n, opts);
|
|
151
|
+
case 'faqItem': return renderFaqItem(n, opts);
|
|
152
|
+
case 'faqQuestion': return `<summary class="pilotiq-faq-question">${renderChildren(n, opts)}</summary>`;
|
|
153
|
+
case 'faqAnswer': return `<div class="pilotiq-faq-answer">${renderChildren(n, opts)}</div>`;
|
|
151
154
|
case 'alert': return renderAlertNode(n, opts);
|
|
152
|
-
case 'prosCons': return
|
|
155
|
+
case 'prosCons': return renderProsCons(n, opts);
|
|
153
156
|
case 'prosColumn': return labeledBlockHtml('pilotiq-pros', 'Pros', n, opts);
|
|
154
157
|
case 'consColumn': return labeledBlockHtml('pilotiq-cons', 'Cons', n, opts);
|
|
155
158
|
case 'mergeTag': return renderMergeTag(n, opts);
|
|
@@ -359,23 +362,85 @@ function clampGridColumnsForRender(raw) {
|
|
|
359
362
|
// Mirrors the editor's `renderHTML` (extensions/contentBlocks.ts): a small
|
|
360
363
|
// label above an editable body. Consumer owns the `pilotiq-*` CSS. Covers
|
|
361
364
|
// keyTakeaways / summary / faq / alert / prosCons (+ pros/cons columns).
|
|
362
|
-
function labeledBlockHtml(cssClass, label, n, opts) {
|
|
363
|
-
|
|
364
|
-
`<div class="pilotiq-block-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
365
|
+
function labeledBlockHtml(cssClass, label, n, opts, wrap = false) {
|
|
366
|
+
const inner = `<div class="pilotiq-block-label">${escapeHtml(label)}</div>` +
|
|
367
|
+
`<div class="pilotiq-block-body">${renderChildren(n, opts)}</div>`;
|
|
368
|
+
// Top-level labelled blocks (summary / keyTakeaways) carry the gear menu, so
|
|
369
|
+
// they get the two-layer anchor: a full-width outer + an inner
|
|
370
|
+
// `.pilotiq-block-content` that holds the max-width / width toggle. Columns
|
|
371
|
+
// inside Pros & cons render flat (no width of their own).
|
|
372
|
+
if (!wrap)
|
|
373
|
+
return `<div class="${cssClass}">${inner}</div>`;
|
|
374
|
+
const width = n.attrs?.['width'] === 'full' ? ' data-width="full"' : '';
|
|
375
|
+
return `<div class="${cssClass}"${width}><div class="pilotiq-block-content">${inner}</div></div>`;
|
|
376
|
+
}
|
|
377
|
+
// Pros & cons — two labelled columns. Same two-layer anchor as the labelled
|
|
378
|
+
// blocks: a full-width outer (`.pilotiq-pros-cons`) + an inner
|
|
379
|
+
// `.pilotiq-pros-cons-content` that carries the grid + width toggle.
|
|
380
|
+
function renderProsCons(n, opts) {
|
|
381
|
+
const width = n.attrs?.['width'] === 'full' ? ' data-width="full"' : '';
|
|
382
|
+
return `<div class="pilotiq-pros-cons"${width}><div class="pilotiq-pros-cons-content">${renderChildren(n, opts)}</div></div>`;
|
|
383
|
+
}
|
|
384
|
+
// FAQ accordion — native, zero-JS `<details>`/`<summary>` per item (mirrors the
|
|
385
|
+
// editor's FaqItem NodeView). Each item's `open` attr drives the platform
|
|
386
|
+
// `open` attribute. Consumer owns the `.pilotiq-faq*` CSS.
|
|
387
|
+
function renderFaqNode(n, opts) {
|
|
388
|
+
const items = (Array.isArray(n.content) ? n.content : []).filter((k) => k?.type === 'faqItem');
|
|
389
|
+
const width = n.attrs?.['width'] === 'full' ? ' data-width="full"' : '';
|
|
390
|
+
return `<div class="pilotiq-faq"${width}><div class="pilotiq-faq-content">${items.map((it) => renderFaqItem(it, opts)).join('')}</div></div>`;
|
|
391
|
+
}
|
|
392
|
+
function renderFaqItem(n, opts) {
|
|
393
|
+
const kids = Array.isArray(n.content) ? n.content : [];
|
|
394
|
+
const q = kids.find((k) => k?.type === 'faqQuestion');
|
|
395
|
+
const a = kids.find((k) => k?.type === 'faqAnswer');
|
|
396
|
+
const open = n.attrs?.['open'] !== false;
|
|
397
|
+
return (`<details class="pilotiq-faq-item"${open ? ' open' : ''}>` +
|
|
398
|
+
`<summary class="pilotiq-faq-question">${q ? renderChildren(q, opts) : ''}</summary>` +
|
|
399
|
+
`<div class="pilotiq-faq-answer">${a ? renderChildren(a, opts) : ''}</div>` +
|
|
400
|
+
`</details>`);
|
|
401
|
+
}
|
|
402
|
+
// shadcn-style callout: icon + editable title + description. Mirrors the
|
|
403
|
+
// editor NodeView (`AlertNodeView`); `coerceAlertType` / `ALERT_ICON_INNER` /
|
|
404
|
+
// labels are shared from `alertVariants` so the two never drift. Consumer owns
|
|
405
|
+
// the `.pilotiq-alert*` CSS.
|
|
370
406
|
function renderAlertNode(n, opts) {
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
407
|
+
const type = coerceAlertType(n.attrs?.['type']);
|
|
408
|
+
const icon = typeof n.attrs?.['icon'] === 'string' ? n.attrs['icon'] : '';
|
|
409
|
+
const iconSvg = typeof n.attrs?.['iconSvg'] === 'string' ? n.attrs['iconSvg'] : '';
|
|
410
|
+
const color = typeof n.attrs?.['color'] === 'string' ? n.attrs['color'] : '';
|
|
411
|
+
const width = n.attrs?.['width'] === 'full' ? ' data-width="full"' : '';
|
|
412
|
+
const kids = Array.isArray(n.content) ? n.content : [];
|
|
413
|
+
const title = kids.find((k) => k?.type === 'alertTitle');
|
|
414
|
+
const body = kids.find((k) => k?.type === 'alertBody');
|
|
415
|
+
const titleHtml = title ? renderChildren(title, opts) : escapeHtml(ALERT_VARIANT_LABEL[type]);
|
|
416
|
+
const bodyHtml = body ? renderChildren(body, opts) : '';
|
|
417
|
+
// Custom variant paints from the chosen color (mirrors the editor's
|
|
418
|
+
// `color-mix` tint); CSS-injection-safe because the value is the parsed
|
|
419
|
+
// attr, only emitted when it matches a strict color literal.
|
|
420
|
+
const tinted = type === 'custom' && isSafeColor(color);
|
|
421
|
+
const style = tinted
|
|
422
|
+
? ` style="border-color:color-mix(in srgb,${color} 35%,transparent);background-color:color-mix(in srgb,${color} 8%,transparent)"`
|
|
423
|
+
: '';
|
|
424
|
+
const iconStyle = tinted ? ` style="color:${color}"` : '';
|
|
425
|
+
// Two layers (mirrors the FAQ block): a full-width `.pilotiq-alert` anchor
|
|
426
|
+
// and an inner `.pilotiq-alert-box` that carries the visual box + the
|
|
427
|
+
// contained/full width. Keeps in-block controls anchored to a stable edge.
|
|
428
|
+
return (`<div class="pilotiq-alert" data-alert-type="${type}"${width}>` +
|
|
429
|
+
`<div class="pilotiq-alert-box pilotiq-alert-${type}" role="note"${style}>` +
|
|
430
|
+
`<span class="pilotiq-alert-icon" aria-hidden="true"${iconStyle}>${buildAlertIconSvg(icon, iconSvg, type)}</span>` +
|
|
431
|
+
`<div class="pilotiq-alert-title">${titleHtml}</div>` +
|
|
432
|
+
`<div class="pilotiq-alert-description">${bodyHtml}</div>` +
|
|
433
|
+
`</div>` +
|
|
377
434
|
`</div>`);
|
|
378
435
|
}
|
|
436
|
+
// Only emit a color into inline CSS when it's a plain literal (hex / rgb(a) /
|
|
437
|
+
// hsl(a) / a CSS keyword) — defends the `style="…"` interpolation against
|
|
438
|
+
// injection via a tampered `color` attr.
|
|
439
|
+
function isSafeColor(value) {
|
|
440
|
+
return /^#[0-9a-fA-F]{3,8}$/.test(value)
|
|
441
|
+
|| /^(rgb|hsl)a?\([0-9.,%\s/]+\)$/.test(value)
|
|
442
|
+
|| /^[a-zA-Z]+$/.test(value);
|
|
443
|
+
}
|
|
379
444
|
// ─── Merge tags + mentions ───────────────────────────────────────────
|
|
380
445
|
/**
|
|
381
446
|
* Render a `mergeTag` atom — either substitute the value from
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pilotiq/tiptap",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.15.0",
|
|
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": {
|
|
@@ -87,11 +87,13 @@
|
|
|
87
87
|
"@types/react-dom": "^19",
|
|
88
88
|
"esbuild": "^0.27",
|
|
89
89
|
"jsdom": "^29.1.1",
|
|
90
|
+
"markdown-it": "^14",
|
|
91
|
+
"@types/markdown-it": "^14",
|
|
90
92
|
"react": "^19",
|
|
91
93
|
"react-dom": "^19",
|
|
92
94
|
"tiptap-markdown": "^0.9",
|
|
93
95
|
"typescript": "^5",
|
|
94
|
-
"@pilotiq/pilotiq": "^0.
|
|
96
|
+
"@pilotiq/pilotiq": "^0.40.0"
|
|
95
97
|
},
|
|
96
98
|
"author": "Suleiman Shahbari",
|
|
97
99
|
"scripts": {
|