@pilotiq/tiptap 3.12.0 → 3.14.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
@@ -1,5 +1,37 @@
1
1
  # @pilotiq/tiptap
2
2
 
3
+ ## 3.14.0
4
+
5
+ ### Minor Changes
6
+
7
+ - b2fc753: Redesigned the **Alert** content block into an interactive, themeable callout — and made it round-trip through the Markdown editor.
8
+
9
+ **Rich-text + markdown editor:**
10
+
11
+ - shadcn-style card on the panel's theme tokens (icon column + editable **title** and **body** — previously the label was the fixed variant name).
12
+ - In-block **variant picker** — `info` / `warning` / `success` / `tip` / **`custom`** (the four slash-menu alert entries collapse into one "Alert").
13
+ - In-block **icon picker** — a curated inline-SVG library (~18 icons, ~1-2KB, no `lucide-react`) plus a **"Custom SVG"** paste field. Custom SVG is sanitized via a pure allowlist (`sanitizeIconSvg`) on input and on render — scripts, event handlers, external refs (`use`/`image`/`a`/`href`), `<style>`, `<foreignObject>` are all stripped.
14
+ - The **custom** variant gets an in-block **color** swatch; the box + icon tint via `color-mix` (the value is validated before it reaches inline CSS).
15
+ - **Markdown round-trip** — `:::alert{type=warning icon=rocket} Title` admonition syntax (title rides the opening fence line). `MarkdownField` gains an **Alert** toolbar button (added to the default toolbar).
16
+
17
+ **Read-side** (`renderRichTextToHtml`) emits the new `<div class="pilotiq-alert"><span class="pilotiq-alert-icon">…</span><div class="pilotiq-alert-title">…</div><div class="pilotiq-alert-description">…</div></div>` structure; icon SVGs are shared with the editor so the two never drift. Consumer owns the CSS.
18
+
19
+ **Back-compat:** the node's content model changed (`block+` → `alertTitle alertBody`). HTML from the previous alert (label + body divs) is parsed into the new shape; JSON-stored alerts from the prior release may lose their body and should be re-inserted.
20
+
21
+ ## 3.13.0
22
+
23
+ ### Minor Changes
24
+
25
+ - ded54a8: Ship five built-in **inline content blocks** in every `RichTextField` (free + pro): **FAQ**, **Alert** (info/warning/success/tip), **Summary**, **Key takeaways**, and **Pros & cons**.
26
+
27
+ They are **inline editable nodes** — a small label on top, content typed straight into the block in place (no card, no popup, no border/background). Inserted from the slash menu's **Content** group. Each renders read-side to semantic `pilotiq-*` HTML via `renderRichTextToHtml` (consumer owns the CSS). Quote and Table remain the native `blockquote` / table extensions.
28
+
29
+ - Nodes live in `extensions/contentBlocks.ts`; registered by default in the editor.
30
+ - Alert's type is the label (Info/Warning/Success/Tip), chosen from the slash menu.
31
+ - Pros & cons is two labelled list columns.
32
+
33
+ The `Block.make().schema([...])` API (card + side-panel form) stays for **custom** blocks via `RichTextField.blocks([...])`, but no schema block ships as a default. `Block.toMeta()` / `RichTextField.toMeta()` are now `async` so option-fields (Select/Radio/ToggleButtons) resolve correctly inside custom schema blocks.
34
+
3
35
  ## 3.12.0
4
36
 
5
37
  ### Minor Changes
package/dist/Block.d.ts CHANGED
@@ -41,7 +41,13 @@ export declare class Block {
41
41
  getLabel(): string;
42
42
  getIcon(): string | undefined;
43
43
  getSchema(): readonly Field[];
44
- /** @internal */
45
- toMeta(): BlockMeta;
44
+ /**
45
+ * @internal
46
+ * Async because schema fields can be async — `SelectField` / `RadioField` /
47
+ * `ToggleButtonsField` resolve their options at meta-build time and return a
48
+ * `Promise<FieldMeta>`. We await every field so the serialized schema never
49
+ * contains an unresolved Promise (which would JSON-stringify to `{}`).
50
+ */
51
+ toMeta(): Promise<BlockMeta>;
46
52
  }
47
53
  //# sourceMappingURL=Block.d.ts.map
package/dist/Block.js CHANGED
@@ -43,13 +43,19 @@ export class Block {
43
43
  getLabel() { return this._label ?? this._name; }
44
44
  getIcon() { return this._icon; }
45
45
  getSchema() { return this._schema; }
46
- /** @internal */
47
- toMeta() {
46
+ /**
47
+ * @internal
48
+ * Async because schema fields can be async — `SelectField` / `RadioField` /
49
+ * `ToggleButtonsField` resolve their options at meta-build time and return a
50
+ * `Promise<FieldMeta>`. We await every field so the serialized schema never
51
+ * contains an unresolved Promise (which would JSON-stringify to `{}`).
52
+ */
53
+ async toMeta() {
48
54
  return {
49
55
  name: this._name,
50
56
  label: this._label ?? this._name,
51
57
  icon: this._icon,
52
- schema: this._schema.map((f) => f.toMeta()),
58
+ schema: (await Promise.all(this._schema.map((f) => f.toMeta()))),
53
59
  };
54
60
  }
55
61
  }
@@ -107,6 +107,7 @@ export interface RichTextFieldMeta extends FieldMeta {
107
107
  */
108
108
  export declare class RichTextField extends Field {
109
109
  private _blocks;
110
+ private _includeDefaultBlocks;
110
111
  private _slashCommand;
111
112
  /** `undefined` = use default, `null` = hidden, array = explicit override. */
112
113
  private _toolbarOverride;
@@ -127,8 +128,22 @@ export declare class RichTextField extends Field {
127
128
  private _mentionsUrl?;
128
129
  private constructor();
129
130
  static make(name: string): RichTextField;
130
- /** Custom blocks available via the slash menu. */
131
+ /** Custom blocks available via the slash menu (merged on top of the defaults). */
131
132
  blocks(blocks: Block[]): this;
133
+ /**
134
+ * Toggle the built-in default blocks (FAQ, Alert, Summary, Key takeaways,
135
+ * Pros & cons) for this field. On by default — every field gets them in the
136
+ * slash menu and the agent block catalog. Pass `false` to ship only the
137
+ * blocks declared via `.blocks([...])`.
138
+ */
139
+ withDefaultBlocks(enabled?: boolean): this;
140
+ /**
141
+ * The blocks this field actually exposes — the default set (unless opted
142
+ * out) merged with the field's own `.blocks([...])`. A field block with the
143
+ * same name overrides the default. This is what the editor slash menu, the
144
+ * side panel, and the agent block catalog all consume.
145
+ */
146
+ private resolveBlocks;
132
147
  /** Toggle the slash menu (`/`) on/off. Defaults to `true`. */
133
148
  slashCommand(enabled: boolean): this;
134
149
  /**
@@ -281,6 +296,6 @@ export declare class RichTextField extends Field {
281
296
  */
282
297
  getToolbarGroups(): ToolbarGroups | null;
283
298
  isFloatingToolbarEnabled(): boolean;
284
- toMeta(ctx?: RenderContext): RichTextFieldMeta;
299
+ toMeta(ctx?: RenderContext): Promise<RichTextFieldMeta>;
285
300
  }
286
301
  //# sourceMappingURL=RichTextField.d.ts.map
@@ -1,4 +1,5 @@
1
1
  import { Field } from '@pilotiq/pilotiq';
2
+ import { defaultBlocks } from './blocks/index.js';
2
3
  /** Default top-level toolbar groups — mirrors the reference admin's layout. */
3
4
  export const DEFAULT_TOOLBAR_GROUPS = [
4
5
  ['bold', 'italic', 'underline', 'strike', 'subscript', 'superscript', 'link'],
@@ -59,6 +60,7 @@ export const DEFAULT_HIGHLIGHT_COLORS = [
59
60
  */
60
61
  export class RichTextField extends Field {
61
62
  _blocks = [];
63
+ _includeDefaultBlocks = true;
62
64
  _slashCommand = true;
63
65
  /** `undefined` = use default, `null` = hidden, array = explicit override. */
64
66
  _toolbarOverride = undefined;
@@ -83,11 +85,37 @@ export class RichTextField extends Field {
83
85
  static make(name) {
84
86
  return new RichTextField(name);
85
87
  }
86
- /** Custom blocks available via the slash menu. */
88
+ /** Custom blocks available via the slash menu (merged on top of the defaults). */
87
89
  blocks(blocks) {
88
90
  this._blocks = blocks;
89
91
  return this;
90
92
  }
93
+ /**
94
+ * Toggle the built-in default blocks (FAQ, Alert, Summary, Key takeaways,
95
+ * Pros & cons) for this field. On by default — every field gets them in the
96
+ * slash menu and the agent block catalog. Pass `false` to ship only the
97
+ * blocks declared via `.blocks([...])`.
98
+ */
99
+ withDefaultBlocks(enabled = true) {
100
+ this._includeDefaultBlocks = enabled;
101
+ return this;
102
+ }
103
+ /**
104
+ * The blocks this field actually exposes — the default set (unless opted
105
+ * out) merged with the field's own `.blocks([...])`. A field block with the
106
+ * same name overrides the default. This is what the editor slash menu, the
107
+ * side panel, and the agent block catalog all consume.
108
+ */
109
+ resolveBlocks() {
110
+ const byName = new Map();
111
+ if (this._includeDefaultBlocks) {
112
+ for (const b of defaultBlocks)
113
+ byName.set(b.getName(), b);
114
+ }
115
+ for (const b of this._blocks)
116
+ byName.set(b.getName(), b);
117
+ return [...byName.values()];
118
+ }
91
119
  /** Toggle the slash menu (`/`) on/off. Defaults to `true`. */
92
120
  slashCommand(enabled) {
93
121
  this._slashCommand = enabled;
@@ -256,7 +284,7 @@ export class RichTextField extends Field {
256
284
  this._mentions = providers;
257
285
  return this;
258
286
  }
259
- getBlocks() { return this._blocks; }
287
+ getBlocks() { return this.resolveBlocks(); }
260
288
  getMergeTags() { return this._mergeTags; }
261
289
  getMentionProviders() { return this._mentions; }
262
290
  /**
@@ -326,10 +354,13 @@ export class RichTextField extends Field {
326
354
  isFloatingToolbarEnabled() {
327
355
  return this._floatingToolbar;
328
356
  }
329
- toMeta(ctx) {
330
- // RichTextField has no async resolvers, so the parent always returns
331
- // the sync FieldMeta branch cast away the union for the spread.
332
- const base = super.toMeta(ctx);
357
+ async toMeta(ctx) {
358
+ // Block fields can be async Select / Radio / ToggleButtons resolve their
359
+ // options at meta-build time and return `Promise<FieldMeta>`. So block
360
+ // metas, and therefore this method, are async. The core resolves every
361
+ // field via `await field.toMeta(ctx)` (`resolveField`), so returning a
362
+ // Promise is the supported path.
363
+ const base = (await Promise.resolve(super.toMeta(ctx)));
333
364
  // Strip `attachFiles` server-side when the panel hasn't registered an
334
365
  // upload adapter — same posture as `MarkdownField` and the editor
335
366
  // chrome stays clean. `uploadUrl` is the wire-side URL for the picker
@@ -342,7 +373,7 @@ export class RichTextField extends Field {
342
373
  .filter(g => g.length > 0) ?? null;
343
374
  return {
344
375
  ...base,
345
- blocks: this._blocks.map((b) => b.toMeta()),
376
+ blocks: await Promise.all(this.resolveBlocks().map((b) => b.toMeta())),
346
377
  slashCommand: this._slashCommand,
347
378
  toolbarGroups: filteredGroups,
348
379
  floatingToolbar: this._floatingToolbar,
@@ -0,0 +1,10 @@
1
+ import { Block } from '../Block.js';
2
+ /**
3
+ * Default **Alert** (callout) block — a typed notice.
4
+ *
5
+ * Wire shape: `{ type: 'info' | 'warning' | 'success' | 'tip'; content: string }`.
6
+ * Renders read-side as `<div class="pilotiq-alert pilotiq-alert-<type>">`. An
7
+ * empty / unknown `type` falls back to `info` at render time.
8
+ */
9
+ export declare const alertBlock: Block;
10
+ //# sourceMappingURL=alert.d.ts.map
@@ -0,0 +1,23 @@
1
+ import { SelectField, TextareaField } from '@pilotiq/pilotiq';
2
+ import { Block } from '../Block.js';
3
+ /**
4
+ * Default **Alert** (callout) block — a typed notice.
5
+ *
6
+ * Wire shape: `{ type: 'info' | 'warning' | 'success' | 'tip'; content: string }`.
7
+ * Renders read-side as `<div class="pilotiq-alert pilotiq-alert-<type>">`. An
8
+ * empty / unknown `type` falls back to `info` at render time.
9
+ */
10
+ export const alertBlock = Block.make('alert')
11
+ .label('Alert')
12
+ .icon('⚠️')
13
+ .schema([
14
+ SelectField.make('type')
15
+ .label('Type')
16
+ .options([
17
+ { value: 'info', label: 'Info' },
18
+ { value: 'warning', label: 'Warning' },
19
+ { value: 'success', label: 'Success' },
20
+ { value: 'tip', label: 'Tip' },
21
+ ]),
22
+ TextareaField.make('content').label('Content').placeholder('Alert message…'),
23
+ ]);
@@ -0,0 +1,9 @@
1
+ import { Block } from '../Block.js';
2
+ /**
3
+ * Default **FAQ** block — a list of question / answer pairs.
4
+ *
5
+ * Wire shape: `{ items: Array<{ question: string; answer: string }> }`.
6
+ * Renders read-side as a `<div class="pilotiq-faq">` of `<details>` items.
7
+ */
8
+ export declare const faqBlock: Block;
9
+ //# sourceMappingURL=faq.d.ts.map
@@ -0,0 +1,21 @@
1
+ import { Repeater, TextField, TextareaField } from '@pilotiq/pilotiq';
2
+ import { Block } from '../Block.js';
3
+ /**
4
+ * Default **FAQ** block — a list of question / answer pairs.
5
+ *
6
+ * Wire shape: `{ items: Array<{ question: string; answer: string }> }`.
7
+ * Renders read-side as a `<div class="pilotiq-faq">` of `<details>` items.
8
+ */
9
+ export const faqBlock = Block.make('faq')
10
+ .label('FAQ')
11
+ .icon('❓')
12
+ .schema([
13
+ Repeater.make('items')
14
+ .label('Questions')
15
+ .schema([
16
+ TextField.make('question').label('Question').placeholder('What is …?'),
17
+ TextareaField.make('answer').label('Answer').placeholder('A short, direct answer.'),
18
+ ])
19
+ .reorderable()
20
+ .addActionLabel('Add question'),
21
+ ]);
@@ -0,0 +1,22 @@
1
+ import type { Block } from '../Block.js';
2
+ /**
3
+ * Schema-form block factories — `Block.make().schema([...])` style, edited via
4
+ * the right-docked side panel. Kept for **custom / legacy** use via
5
+ * `RichTextField.blocks([...])`. They are NO LONGER defaults.
6
+ *
7
+ * The built-in content blocks — FAQ, Alert, Summary, Key takeaways, Pros & cons
8
+ * — are now **inline editable nodes** (`extensions/contentBlocks.ts`):
9
+ * labelled, edited in place, registered directly in the editor + slash menu.
10
+ */
11
+ export { faqBlock } from './faq.js';
12
+ export { alertBlock } from './alert.js';
13
+ export { summaryBlock } from './summary.js';
14
+ export { keyTakeawaysBlock } from './keyTakeaways.js';
15
+ export { prosConsBlock } from './prosCons.js';
16
+ /**
17
+ * Schema blocks shipped by default in every `RichTextField`. Empty — the
18
+ * default content blocks are inline nodes now, not schema-form blocks. A
19
+ * field's own `.blocks([...])` still merge in via `RichTextField`.
20
+ */
21
+ export declare const defaultBlocks: readonly Block[];
22
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Schema-form block factories — `Block.make().schema([...])` style, edited via
3
+ * the right-docked side panel. Kept for **custom / legacy** use via
4
+ * `RichTextField.blocks([...])`. They are NO LONGER defaults.
5
+ *
6
+ * The built-in content blocks — FAQ, Alert, Summary, Key takeaways, Pros & cons
7
+ * — are now **inline editable nodes** (`extensions/contentBlocks.ts`):
8
+ * labelled, edited in place, registered directly in the editor + slash menu.
9
+ */
10
+ export { faqBlock } from './faq.js';
11
+ export { alertBlock } from './alert.js';
12
+ export { summaryBlock } from './summary.js';
13
+ export { keyTakeawaysBlock } from './keyTakeaways.js';
14
+ export { prosConsBlock } from './prosCons.js';
15
+ /**
16
+ * Schema blocks shipped by default in every `RichTextField`. Empty — the
17
+ * default content blocks are inline nodes now, not schema-form blocks. A
18
+ * field's own `.blocks([...])` still merge in via `RichTextField`.
19
+ */
20
+ export const defaultBlocks = [];
@@ -0,0 +1,10 @@
1
+ import { Block } from '../Block.js';
2
+ /**
3
+ * Default **Key takeaways** block — a short bulleted list of the article's
4
+ * main points.
5
+ *
6
+ * Wire shape: `{ points: string[] }`. Renders read-side as
7
+ * `<div class="pilotiq-key-takeaways">` with a label and a `<ul>`.
8
+ */
9
+ export declare const keyTakeawaysBlock: Block;
10
+ //# sourceMappingURL=keyTakeaways.d.ts.map
@@ -0,0 +1,15 @@
1
+ import { TagsInput } from '@pilotiq/pilotiq';
2
+ import { Block } from '../Block.js';
3
+ /**
4
+ * Default **Key takeaways** block — a short bulleted list of the article's
5
+ * main points.
6
+ *
7
+ * Wire shape: `{ points: string[] }`. Renders read-side as
8
+ * `<div class="pilotiq-key-takeaways">` with a label and a `<ul>`.
9
+ */
10
+ export const keyTakeawaysBlock = Block.make('key-takeaways')
11
+ .label('Key takeaways')
12
+ .icon('🔑')
13
+ .schema([
14
+ TagsInput.make('points').label('Key takeaways'),
15
+ ]);
@@ -0,0 +1,9 @@
1
+ import { Block } from '../Block.js';
2
+ /**
3
+ * Default **Pros & cons** block — two lists, side by side.
4
+ *
5
+ * Wire shape: `{ pros: string[]; cons: string[] }`. Renders read-side as
6
+ * `<div class="pilotiq-pros-cons">` with a `pros` and a `cons` column.
7
+ */
8
+ export declare const prosConsBlock: Block;
9
+ //# sourceMappingURL=prosCons.d.ts.map
@@ -0,0 +1,15 @@
1
+ import { TagsInput } from '@pilotiq/pilotiq';
2
+ import { Block } from '../Block.js';
3
+ /**
4
+ * Default **Pros & cons** block — two lists, side by side.
5
+ *
6
+ * Wire shape: `{ pros: string[]; cons: string[] }`. Renders read-side as
7
+ * `<div class="pilotiq-pros-cons">` with a `pros` and a `cons` column.
8
+ */
9
+ export const prosConsBlock = Block.make('pros-cons')
10
+ .label('Pros & cons')
11
+ .icon('⚖️')
12
+ .schema([
13
+ TagsInput.make('pros').label('Pros'),
14
+ TagsInput.make('cons').label('Cons'),
15
+ ]);
@@ -0,0 +1,9 @@
1
+ import { Block } from '../Block.js';
2
+ /**
3
+ * Default **Summary** block — a short, labelled summary of the article.
4
+ *
5
+ * Wire shape: `{ content: string }`. Renders read-side as
6
+ * `<div class="pilotiq-summary">` with a label and the body.
7
+ */
8
+ export declare const summaryBlock: Block;
9
+ //# sourceMappingURL=summary.d.ts.map
@@ -0,0 +1,14 @@
1
+ import { TextareaField } from '@pilotiq/pilotiq';
2
+ import { Block } from '../Block.js';
3
+ /**
4
+ * Default **Summary** block — a short, labelled summary of the article.
5
+ *
6
+ * Wire shape: `{ content: string }`. Renders read-side as
7
+ * `<div class="pilotiq-summary">` with a label and the body.
8
+ */
9
+ export const summaryBlock = Block.make('summary')
10
+ .label('Summary')
11
+ .icon('📝')
12
+ .schema([
13
+ TextareaField.make('content').label('Summary').placeholder('A short summary of the article…'),
14
+ ]);
@@ -34,11 +34,14 @@ export const DragHandleExtension = Extension.create({
34
34
  },
35
35
  });
36
36
  function createDragHandleView(view) {
37
- const handle = document.createElement('button');
38
- handle.type = 'button';
37
+ // A <div> (not <button>): a button grabs DOM focus on click, which yanks
38
+ // focus out of the editor so a follow-up Backspace/Delete on the selected
39
+ // block goes nowhere. A div doesn't steal focus, so click-to-select works.
40
+ const handle = document.createElement('div');
41
+ handle.setAttribute('role', 'button');
39
42
  handle.setAttribute('data-pilotiq-drag-handle', '');
40
43
  handle.setAttribute('contenteditable', 'false');
41
- handle.setAttribute('aria-label', 'Drag block');
44
+ handle.setAttribute('aria-label', 'Drag or select block');
42
45
  handle.setAttribute('draggable', 'true');
43
46
  handle.style.cssText = [
44
47
  'position: absolute',
@@ -149,16 +152,30 @@ function createDragHandleView(view) {
149
152
  const onDragEnd = () => {
150
153
  handle.style.cursor = 'grab';
151
154
  };
155
+ // A plain click (no drag) selects the hovered block as a NodeSelection, so
156
+ // the whole block can be deleted (Backspace / Delete) or replaced — the only
157
+ // affordance for removing atom-ish custom blocks (FAQ, callout, grid, …).
158
+ const onClick = (event) => {
159
+ event.preventDefault();
160
+ if (activePos === null)
161
+ return;
162
+ if (!view.state.doc.nodeAt(activePos))
163
+ return;
164
+ view.dispatch(view.state.tr.setSelection(NodeSelection.create(view.state.doc, activePos)));
165
+ view.focus();
166
+ };
152
167
  view.dom.addEventListener('mousemove', onMouseMove);
153
168
  view.dom.addEventListener('mouseleave', onMouseLeave);
154
169
  handle.addEventListener('dragstart', onDragStart);
155
170
  handle.addEventListener('dragend', onDragEnd);
171
+ handle.addEventListener('click', onClick);
156
172
  return {
157
173
  destroy: () => {
158
174
  view.dom.removeEventListener('mousemove', onMouseMove);
159
175
  view.dom.removeEventListener('mouseleave', onMouseLeave);
160
176
  handle.removeEventListener('dragstart', onDragStart);
161
177
  handle.removeEventListener('dragend', onDragEnd);
178
+ handle.removeEventListener('click', onClick);
162
179
  handle.remove();
163
180
  },
164
181
  };
@@ -137,6 +137,72 @@ export function buildSlashItems(blocks, mergeTags, query, insert) {
137
137
  searchKey: 'grid columns layout 3 three split',
138
138
  command: ({ editor, range }) => editor.chain().focus().deleteRange(range).setGrid({ columns: 3 }).run(),
139
139
  },
140
+ // Inline content blocks — labelled, editable-in-place regions (nodes in
141
+ // contentBlocks.ts). Inserted via insertContent; no custom commands.
142
+ {
143
+ key: 'key-takeaways', label: 'Key takeaways', icon: '🔑', group: 'Content',
144
+ searchKey: 'key takeaways points highlights tldr',
145
+ command: ({ editor, range }) => editor.chain().focus().deleteRange(range).insertContent({
146
+ type: 'keyTakeaways',
147
+ content: [{ type: 'bulletList', content: [{ type: 'listItem', content: [{ type: 'paragraph' }] }] }],
148
+ }).run(),
149
+ },
150
+ {
151
+ key: 'summary', label: 'Summary', icon: '📝', group: 'Content',
152
+ searchKey: 'summary tldr abstract overview',
153
+ command: ({ editor, range }) => editor.chain().focus().deleteRange(range).insertContent({ type: 'summary', content: [{ type: 'paragraph' }] }).run(),
154
+ },
155
+ {
156
+ key: 'faq', label: 'FAQ', icon: '❓', group: 'Content',
157
+ searchKey: 'faq questions answers frequently asked',
158
+ command: ({ editor, range }) => {
159
+ editor.chain().focus().deleteRange(range).insertContent({
160
+ type: 'faq',
161
+ content: [
162
+ {
163
+ type: 'faqItem',
164
+ content: [{ type: 'faqQuestion' }, { type: 'faqAnswer', content: [{ type: 'paragraph' }] }],
165
+ },
166
+ ],
167
+ }).run();
168
+ // insertContent leaves the cursor in the answer — move it into the first
169
+ // question. Resolve the enclosing faqItem and select its question start
170
+ // (before(faqItem) +1 into the item +1 into the question = +2).
171
+ const { $from } = editor.state.selection;
172
+ for (let d = $from.depth; d > 0; d--) {
173
+ if ($from.node(d).type.name === 'faqItem') {
174
+ editor.chain().setTextSelection($from.before(d) + 2).focus().run();
175
+ break;
176
+ }
177
+ }
178
+ },
179
+ },
180
+ {
181
+ key: 'pros-cons', label: 'Pros & cons', icon: '⚖️', group: 'Content',
182
+ searchKey: 'pros cons advantages disadvantages comparison',
183
+ command: ({ editor, range }) => editor.chain().focus().deleteRange(range).insertContent({
184
+ type: 'prosCons',
185
+ content: [
186
+ { type: 'prosColumn', content: [{ type: 'bulletList', content: [{ type: 'listItem', content: [{ type: 'paragraph' }] }] }] },
187
+ { type: 'consColumn', content: [{ type: 'bulletList', content: [{ type: 'listItem', content: [{ type: 'paragraph' }] }] }] },
188
+ ],
189
+ }).run(),
190
+ },
191
+ {
192
+ // One Alert block — the variant (info/warning/success/tip/custom) is
193
+ // switched in-block via the NodeView's picker. Defaults to Info with the
194
+ // label pre-filled as the editable title.
195
+ key: 'alert', label: 'Alert', icon: '⚠️', group: 'Content',
196
+ searchKey: 'alert callout notice info warning success tip note custom caution danger hint',
197
+ command: ({ editor, range }) => editor.chain().focus().deleteRange(range).insertContent({
198
+ type: 'alert',
199
+ attrs: { type: 'info' },
200
+ content: [
201
+ { type: 'alertTitle', content: [{ type: 'text', text: 'Info' }] },
202
+ { type: 'alertBody', content: [{ type: 'paragraph' }] },
203
+ ],
204
+ }).run(),
205
+ },
140
206
  // Image entry shares the toolbar's attach-files dialog; only surfaced
141
207
  // when the panel has wired an `UploadAdapter`. Without one, the dialog
142
208
  // would post to a missing endpoint — the slash item degrades the same
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Alert variant primitives — shared by the node (`contentBlocks.ts`), its
3
+ * React NodeView (`AlertNodeView.tsx`), and the read-side serializer
4
+ * (`render.ts`). Kept dependency-free (no `@tiptap/*`) so the server
5
+ * renderer can import it without pulling a Tiptap runtime.
6
+ *
7
+ * `info` / `warning` / `success` / `tip` are presets (default icon + accent);
8
+ * `custom` is the user-driven variant (own icon + color, Phase 2).
9
+ */
10
+ export declare const ALERT_VARIANTS: readonly ["info", "warning", "success", "tip", "custom"];
11
+ export type AlertType = (typeof ALERT_VARIANTS)[number];
12
+ export declare const ALERT_VARIANT_LABEL: Record<AlertType, string>;
13
+ /** Coerce an unknown value to a known variant, defaulting to `info`. */
14
+ export declare function coerceAlertType(value: unknown): AlertType;
15
+ /**
16
+ * Curated icon library — inner SVG markup (lucide paths) keyed by name. Shared
17
+ * by the NodeView picker (`AlertNodeView`) and the read-side renderer
18
+ * (`render.ts`) so the editor and published output never drift. Hand-picked
19
+ * (no `lucide-react` dep, no icon font) so the whole set is ~1-2KB gzipped.
20
+ * Wrap in `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"
21
+ * stroke-width="2" stroke-linecap="round" stroke-linejoin="round">…</svg>`.
22
+ */
23
+ export declare const ALERT_ICONS: Record<string, string>;
24
+ /** Icon keys, in picker display order. */
25
+ export declare const ALERT_ICON_KEYS: string[];
26
+ /** The default icon key per variant (used when the block sets no `icon`). */
27
+ export declare const VARIANT_DEFAULT_ICON: Record<AlertType, string>;
28
+ /**
29
+ * Resolve the inner SVG for a block: the explicit `icon` override when set +
30
+ * known, else the variant's default. Shared by the editor + read-side render.
31
+ */
32
+ export declare function resolveAlertIconInner(iconKey: string | undefined, variant: AlertType): string;
33
+ /**
34
+ * Sanitize a user-pasted SVG string to a safe subset. Returns '' for anything
35
+ * that isn't an `<svg>` element or that sanitizes to nothing.
36
+ */
37
+ export declare function sanitizeIconSvg(raw: unknown): string;
38
+ /**
39
+ * Build the FULL `<svg>` string for a block: a sanitized user `iconSvg` when
40
+ * present, else the (library or variant-default) icon wrapped in the standard
41
+ * stroke svg. Shared by the editor NodeView and the read-side renderer.
42
+ */
43
+ export declare function buildAlertIconSvg(iconKey: string | undefined, iconSvg: string | undefined, variant: AlertType): string;
44
+ //# sourceMappingURL=alertVariants.d.ts.map