@pilotiq/tiptap 3.19.2 → 4.0.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/BlockNodeExtension.d.ts +0 -9
- package/dist/extensions/BlockNodeExtension.js +0 -20
- package/dist/extensions/{AiInlineDiffExtension.d.ts → InlineDiffExtension.d.ts} +23 -23
- package/dist/extensions/{AiInlineDiffExtension.js → InlineDiffExtension.js} +33 -33
- package/dist/extensions/{AiSuggestionExtension.d.ts → SuggestionChipExtension.d.ts} +29 -29
- package/dist/extensions/{AiSuggestionExtension.js → SuggestionChipExtension.js} +52 -52
- package/dist/index.d.ts +4 -4
- package/dist/index.js +4 -4
- package/dist/react/BlockNodeView.d.ts +23 -12
- package/dist/react/BlockNodeView.js +55 -21
- package/dist/react/CollabTextRenderer.js +16 -16
- package/dist/react/MarkdownEditor.js +17 -17
- package/dist/react/{AiSuggestionBanner.d.ts → SuggestionBanner.d.ts} +6 -6
- package/dist/react/{AiSuggestionBanner.js → SuggestionBanner.js} +5 -5
- package/dist/react/TiptapEditor.js +24 -59
- package/dist/react/blockValues.d.ts +54 -0
- package/dist/react/blockValues.js +161 -0
- package/dist/react/floatingToolbarVisibility.d.ts +9 -2
- package/dist/react/floatingToolbarVisibility.js +12 -3
- package/dist/react/{useAiInlineDiff.d.ts → useInlineDiff.d.ts} +13 -13
- package/dist/react/{useAiInlineDiff.js → useInlineDiff.js} +36 -19
- package/dist/react/{useAiSuggestionBridge.d.ts → useSuggestionBridge.d.ts} +7 -7
- package/dist/react/{useAiSuggestionBridge.js → useSuggestionBridge.js} +10 -10
- package/dist/surgicalOps.d.ts +15 -2
- package/dist/surgicalOps.js +50 -3
- package/package.json +1 -1
- package/dist/react/BlockSidePanel.d.ts +0 -105
- package/dist/react/BlockSidePanel.js +0 -338
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Extension } from '@tiptap/core';
|
|
2
2
|
import { Plugin, PluginKey } from '@tiptap/pm/state';
|
|
3
3
|
import { Decoration, DecorationSet } from '@tiptap/pm/view';
|
|
4
|
-
export const
|
|
4
|
+
export const suggestionChipPluginKey = new PluginKey('pilotiqSuggestionChip');
|
|
5
5
|
/**
|
|
6
6
|
* Append or replace by id. Pure — exported for tests and so the same dedupe
|
|
7
7
|
* shape can drive consumer-side mirror state.
|
|
@@ -55,7 +55,7 @@ export function sortForApproveAll(suggestions) {
|
|
|
55
55
|
*
|
|
56
56
|
* Usage:
|
|
57
57
|
* ```ts
|
|
58
|
-
* editor.commands.
|
|
58
|
+
* editor.commands.addSuggestion({
|
|
59
59
|
* id: 'seo-1',
|
|
60
60
|
* from: 12,
|
|
61
61
|
* to: 18,
|
|
@@ -63,30 +63,30 @@ export function sortForApproveAll(suggestions) {
|
|
|
63
63
|
* source: { agentLabel: 'SEO' },
|
|
64
64
|
* })
|
|
65
65
|
* // …user clicks ✓ on the chip, or:
|
|
66
|
-
* editor.commands.
|
|
66
|
+
* editor.commands.approveSuggestion('seo-1')
|
|
67
67
|
* ```
|
|
68
68
|
*
|
|
69
69
|
* Mounted by default inside `TiptapEditor`; consumer code reaches it through
|
|
70
70
|
* the editor's command surface.
|
|
71
71
|
*/
|
|
72
|
-
export const
|
|
73
|
-
name: '
|
|
72
|
+
export const SuggestionChipExtension = Extension.create({
|
|
73
|
+
name: 'pilotiqSuggestionChip',
|
|
74
74
|
addOptions() {
|
|
75
75
|
return {
|
|
76
|
-
classPrefix: 'pilotiq-
|
|
76
|
+
classPrefix: 'pilotiq-suggestion',
|
|
77
77
|
};
|
|
78
78
|
},
|
|
79
79
|
onCreate() {
|
|
80
80
|
// Inject minimal default styles for the chip + strikethrough on first
|
|
81
81
|
// mount so consumers see the visualization without wiring CSS. Idempotent
|
|
82
|
-
// via the `data-pilotiq-
|
|
82
|
+
// via the `data-pilotiq-suggestion-styles` sentinel; consumers who
|
|
83
83
|
// want full control just add their own `<style>` with the same class
|
|
84
84
|
// names (last wins — the cascade picks user overrides over our defaults
|
|
85
85
|
// since the user stylesheet appears AFTER our injected one in `<head>`
|
|
86
86
|
// when imported via Vite/Webpack, OR via higher specificity).
|
|
87
87
|
if (typeof document === 'undefined')
|
|
88
88
|
return;
|
|
89
|
-
const SENTINEL = 'data-pilotiq-
|
|
89
|
+
const SENTINEL = 'data-pilotiq-suggestion-styles';
|
|
90
90
|
if (document.head.querySelector(`style[${SENTINEL}]`))
|
|
91
91
|
return;
|
|
92
92
|
const prefix = this.options.classPrefix;
|
|
@@ -134,10 +134,10 @@ export const AiSuggestionExtension = Extension.create({
|
|
|
134
134
|
/* Banner — bottom-of-editor strip for whole-field suggestions on rich
|
|
135
135
|
surfaces (markdown / richtext). Sibling to the chip styles above;
|
|
136
136
|
lives here so both ship via the same extension-mount sentinel.
|
|
137
|
-
Class names live under \`pilotiq-
|
|
137
|
+
Class names live under \`pilotiq-suggestion-banner-*\` (not \`-suggestion-\`)
|
|
138
138
|
since the banner is a host-mounted React component, not a PM
|
|
139
139
|
decoration. */
|
|
140
|
-
.pilotiq-
|
|
140
|
+
.pilotiq-suggestion-banner {
|
|
141
141
|
display: flex;
|
|
142
142
|
align-items: center;
|
|
143
143
|
gap: 0.5rem;
|
|
@@ -150,15 +150,15 @@ export const AiSuggestionExtension = Extension.create({
|
|
|
150
150
|
font-size: 0.875rem;
|
|
151
151
|
line-height: 1.4;
|
|
152
152
|
}
|
|
153
|
-
.pilotiq-
|
|
154
|
-
.pilotiq-
|
|
155
|
-
.pilotiq-
|
|
153
|
+
.pilotiq-suggestion-banner-icon { flex: 0 0 auto; }
|
|
154
|
+
.pilotiq-suggestion-banner-label { flex: 1 1 auto; }
|
|
155
|
+
.pilotiq-suggestion-banner-actions {
|
|
156
156
|
display: inline-flex;
|
|
157
157
|
gap: 0.375rem;
|
|
158
158
|
flex: 0 0 auto;
|
|
159
159
|
}
|
|
160
|
-
.pilotiq-
|
|
161
|
-
.pilotiq-
|
|
160
|
+
.pilotiq-suggestion-banner-reject,
|
|
161
|
+
.pilotiq-suggestion-banner-accept {
|
|
162
162
|
appearance: none;
|
|
163
163
|
cursor: pointer;
|
|
164
164
|
font-size: 0.8125rem;
|
|
@@ -168,50 +168,50 @@ export const AiSuggestionExtension = Extension.create({
|
|
|
168
168
|
border-radius: 0.25rem;
|
|
169
169
|
border: 1px solid transparent;
|
|
170
170
|
}
|
|
171
|
-
.pilotiq-
|
|
171
|
+
.pilotiq-suggestion-banner-reject {
|
|
172
172
|
background-color: transparent;
|
|
173
173
|
color: rgb(120, 53, 15);
|
|
174
174
|
border-color: rgba(180, 83, 9, 0.4);
|
|
175
175
|
}
|
|
176
|
-
.pilotiq-
|
|
176
|
+
.pilotiq-suggestion-banner-reject:hover {
|
|
177
177
|
background-color: rgba(254, 215, 170, 0.4);
|
|
178
178
|
}
|
|
179
|
-
.pilotiq-
|
|
179
|
+
.pilotiq-suggestion-banner-accept {
|
|
180
180
|
background-color: rgb(22, 101, 52);
|
|
181
181
|
color: white;
|
|
182
182
|
}
|
|
183
|
-
.pilotiq-
|
|
183
|
+
.pilotiq-suggestion-banner-accept:hover { background-color: rgb(21, 128, 61); }
|
|
184
184
|
`;
|
|
185
185
|
document.head.appendChild(style);
|
|
186
186
|
},
|
|
187
187
|
addCommands() {
|
|
188
188
|
return {
|
|
189
|
-
|
|
190
|
-
const current =
|
|
189
|
+
addSuggestion: (suggestion) => ({ tr, state, dispatch }) => {
|
|
190
|
+
const current = suggestionChipPluginKey.getState(state)?.suggestions ?? [];
|
|
191
191
|
const next = upsertSuggestion(current, suggestion);
|
|
192
192
|
if (dispatch) {
|
|
193
|
-
tr.setMeta(
|
|
193
|
+
tr.setMeta(suggestionChipPluginKey, { type: 'set', next });
|
|
194
194
|
dispatch(tr);
|
|
195
195
|
}
|
|
196
196
|
return true;
|
|
197
197
|
},
|
|
198
|
-
|
|
199
|
-
const current =
|
|
198
|
+
addSuggestions: (suggestions) => ({ tr, state, dispatch }) => {
|
|
199
|
+
const current = suggestionChipPluginKey.getState(state)?.suggestions ?? [];
|
|
200
200
|
const next = upsertSuggestions(current, suggestions);
|
|
201
201
|
if (dispatch) {
|
|
202
|
-
tr.setMeta(
|
|
202
|
+
tr.setMeta(suggestionChipPluginKey, { type: 'set', next });
|
|
203
203
|
dispatch(tr);
|
|
204
204
|
}
|
|
205
205
|
return true;
|
|
206
206
|
},
|
|
207
|
-
|
|
208
|
-
const current =
|
|
207
|
+
approveSuggestion: (id) => ({ tr, state, dispatch }) => {
|
|
208
|
+
const current = suggestionChipPluginKey.getState(state)?.suggestions ?? [];
|
|
209
209
|
const target = current.find(s => s.id === id);
|
|
210
210
|
if (!target)
|
|
211
211
|
return false;
|
|
212
212
|
if (dispatch) {
|
|
213
213
|
applyApprove(tr, state, target);
|
|
214
|
-
tr.setMeta(
|
|
214
|
+
tr.setMeta(suggestionChipPluginKey, {
|
|
215
215
|
type: 'set',
|
|
216
216
|
next: removeSuggestion(current, id),
|
|
217
217
|
});
|
|
@@ -219,13 +219,13 @@ export const AiSuggestionExtension = Extension.create({
|
|
|
219
219
|
}
|
|
220
220
|
return true;
|
|
221
221
|
},
|
|
222
|
-
|
|
223
|
-
const current =
|
|
222
|
+
rejectSuggestion: (id) => ({ tr, state, dispatch }) => {
|
|
223
|
+
const current = suggestionChipPluginKey.getState(state)?.suggestions ?? [];
|
|
224
224
|
const target = current.find(s => s.id === id);
|
|
225
225
|
if (!target)
|
|
226
226
|
return false;
|
|
227
227
|
if (dispatch) {
|
|
228
|
-
tr.setMeta(
|
|
228
|
+
tr.setMeta(suggestionChipPluginKey, {
|
|
229
229
|
type: 'set',
|
|
230
230
|
next: removeSuggestion(current, id),
|
|
231
231
|
});
|
|
@@ -233,14 +233,14 @@ export const AiSuggestionExtension = Extension.create({
|
|
|
233
233
|
}
|
|
234
234
|
return true;
|
|
235
235
|
},
|
|
236
|
-
|
|
237
|
-
const current =
|
|
236
|
+
approveAllSuggestions: () => ({ tr, state, dispatch }) => {
|
|
237
|
+
const current = suggestionChipPluginKey.getState(state)?.suggestions ?? [];
|
|
238
238
|
if (current.length === 0)
|
|
239
239
|
return false;
|
|
240
240
|
if (dispatch) {
|
|
241
241
|
for (const s of sortForApproveAll(current))
|
|
242
242
|
applyApprove(tr, state, s);
|
|
243
|
-
tr.setMeta(
|
|
243
|
+
tr.setMeta(suggestionChipPluginKey, {
|
|
244
244
|
type: 'set',
|
|
245
245
|
next: [],
|
|
246
246
|
});
|
|
@@ -248,12 +248,12 @@ export const AiSuggestionExtension = Extension.create({
|
|
|
248
248
|
}
|
|
249
249
|
return true;
|
|
250
250
|
},
|
|
251
|
-
|
|
252
|
-
const current =
|
|
251
|
+
rejectAllSuggestions: () => ({ tr, state, dispatch }) => {
|
|
252
|
+
const current = suggestionChipPluginKey.getState(state)?.suggestions ?? [];
|
|
253
253
|
if (current.length === 0)
|
|
254
254
|
return false;
|
|
255
255
|
if (dispatch) {
|
|
256
|
-
tr.setMeta(
|
|
256
|
+
tr.setMeta(suggestionChipPluginKey, {
|
|
257
257
|
type: 'set',
|
|
258
258
|
next: [],
|
|
259
259
|
});
|
|
@@ -261,12 +261,12 @@ export const AiSuggestionExtension = Extension.create({
|
|
|
261
261
|
}
|
|
262
262
|
return true;
|
|
263
263
|
},
|
|
264
|
-
|
|
265
|
-
const current =
|
|
264
|
+
clearSuggestions: () => ({ tr, state, dispatch }) => {
|
|
265
|
+
const current = suggestionChipPluginKey.getState(state)?.suggestions ?? [];
|
|
266
266
|
if (current.length === 0)
|
|
267
267
|
return false;
|
|
268
268
|
if (dispatch) {
|
|
269
|
-
tr.setMeta(
|
|
269
|
+
tr.setMeta(suggestionChipPluginKey, {
|
|
270
270
|
type: 'set',
|
|
271
271
|
next: [],
|
|
272
272
|
});
|
|
@@ -280,11 +280,11 @@ export const AiSuggestionExtension = Extension.create({
|
|
|
280
280
|
const ext = this;
|
|
281
281
|
return [
|
|
282
282
|
new Plugin({
|
|
283
|
-
key:
|
|
283
|
+
key: suggestionChipPluginKey,
|
|
284
284
|
state: {
|
|
285
285
|
init: () => ({ suggestions: [] }),
|
|
286
286
|
apply(tr, prev) {
|
|
287
|
-
const meta = tr.getMeta(
|
|
287
|
+
const meta = tr.getMeta(suggestionChipPluginKey);
|
|
288
288
|
const base = meta?.type === 'set' ? meta.next : prev.suggestions;
|
|
289
289
|
if (!tr.docChanged)
|
|
290
290
|
return { suggestions: base };
|
|
@@ -295,17 +295,17 @@ export const AiSuggestionExtension = Extension.create({
|
|
|
295
295
|
},
|
|
296
296
|
props: {
|
|
297
297
|
decorations(state) {
|
|
298
|
-
const ps =
|
|
298
|
+
const ps = suggestionChipPluginKey.getState(state);
|
|
299
299
|
if (!ps || ps.suggestions.length === 0)
|
|
300
300
|
return DecorationSet.empty;
|
|
301
301
|
return buildDecorations(state, ps.suggestions, ext.options.classPrefix, ext.editor);
|
|
302
302
|
},
|
|
303
303
|
},
|
|
304
304
|
view(view) {
|
|
305
|
-
let last =
|
|
305
|
+
let last = suggestionChipPluginKey.getState(view.state)?.suggestions;
|
|
306
306
|
return {
|
|
307
307
|
update(updated) {
|
|
308
|
-
const next =
|
|
308
|
+
const next = suggestionChipPluginKey.getState(updated.state)?.suggestions;
|
|
309
309
|
if (next === last)
|
|
310
310
|
return;
|
|
311
311
|
last = next;
|
|
@@ -344,13 +344,13 @@ function buildDecorations(state, suggestions, prefix, editor) {
|
|
|
344
344
|
if (from < to) {
|
|
345
345
|
decos.push(Decoration.inline(from, to, {
|
|
346
346
|
class: `${prefix}-original`,
|
|
347
|
-
'data-pilotiq-
|
|
347
|
+
'data-pilotiq-suggestion-id': s.id,
|
|
348
348
|
}));
|
|
349
349
|
}
|
|
350
350
|
decos.push(Decoration.widget(to, () => buildChip(s, prefix, editor), {
|
|
351
351
|
side: 1,
|
|
352
352
|
ignoreSelection: true,
|
|
353
|
-
key: `pilotiq-
|
|
353
|
+
key: `pilotiq-suggestion:${s.id}`,
|
|
354
354
|
}));
|
|
355
355
|
}
|
|
356
356
|
return DecorationSet.create(state.doc, decos);
|
|
@@ -368,7 +368,7 @@ export function clampPos(pos, max) {
|
|
|
368
368
|
function buildChip(s, prefix, editor) {
|
|
369
369
|
const root = document.createElement('span');
|
|
370
370
|
root.className = `${prefix}-chip`;
|
|
371
|
-
root.setAttribute('data-pilotiq-
|
|
371
|
+
root.setAttribute('data-pilotiq-suggestion-id', s.id);
|
|
372
372
|
root.contentEditable = 'false';
|
|
373
373
|
if (s.replacement.length > 0) {
|
|
374
374
|
const insert = document.createElement('span');
|
|
@@ -377,16 +377,16 @@ function buildChip(s, prefix, editor) {
|
|
|
377
377
|
root.appendChild(insert);
|
|
378
378
|
}
|
|
379
379
|
if (s.source?.agentLabel) {
|
|
380
|
-
root.setAttribute('data-pilotiq-
|
|
380
|
+
root.setAttribute('data-pilotiq-suggestion-source', s.source.agentLabel);
|
|
381
381
|
}
|
|
382
382
|
if (s.source?.agentSlug) {
|
|
383
|
-
root.setAttribute('data-pilotiq-
|
|
383
|
+
root.setAttribute('data-pilotiq-suggestion-source-slug', s.source.agentSlug);
|
|
384
384
|
}
|
|
385
385
|
root.appendChild(buildButton(prefix, 'accept', '✓', 'Accept suggestion', () => {
|
|
386
|
-
editor.chain().focus().
|
|
386
|
+
editor.chain().focus().approveSuggestion(s.id).run();
|
|
387
387
|
}));
|
|
388
388
|
root.appendChild(buildButton(prefix, 'reject', '✕', 'Reject suggestion', () => {
|
|
389
|
-
editor.chain().focus().
|
|
389
|
+
editor.chain().focus().rejectSuggestion(s.id).run();
|
|
390
390
|
}));
|
|
391
391
|
return root;
|
|
392
392
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -6,10 +6,10 @@ export { registerTiptap } from './register.js';
|
|
|
6
6
|
export { createPlainTextEditor, plainTextOf, plainTextToDoc, type PlainTextEditorOptions, } from './PlainTextEditor.js';
|
|
7
7
|
export { tiptap } from './plugin.js';
|
|
8
8
|
export { TiptapEditor } from './react/TiptapEditor.js';
|
|
9
|
-
export {
|
|
10
|
-
export {
|
|
11
|
-
export {
|
|
12
|
-
export { planReplaceBlock, planInsertBlockBefore, planDeleteBlock, planWrapBlocks, planUpdateBlockMark, summarizeBlockStructure, type BlockMarkRange, type TransactionModifier, } from './surgicalOps.js';
|
|
9
|
+
export { SuggestionChipExtension, suggestionChipPluginKey, upsertSuggestion, upsertSuggestions, removeSuggestion, remapSuggestions, sortForApproveAll, clampPos, type InlineSuggestion, type SuggestionChipExtensionOptions, } from './extensions/SuggestionChipExtension.js';
|
|
10
|
+
export { useSuggestionBridge } from './react/useSuggestionBridge.js';
|
|
11
|
+
export { InlineDiffExtension, inlineDiffPluginKey, getInlineDiffState, type InlineDiffExtensionOptions, } from './extensions/InlineDiffExtension.js';
|
|
12
|
+
export { planReplaceBlock, planInsertBlockBefore, planDeleteBlock, planWrapBlocks, planUpdateBlockMark, planReplaceText, 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
15
|
export { shouldShowFloatingToolbar, TOOLBAR_MARKS } from './react/floatingToolbarVisibility.js';
|
package/dist/index.js
CHANGED
|
@@ -6,10 +6,10 @@ export { registerTiptap } from './register.js';
|
|
|
6
6
|
export { createPlainTextEditor, plainTextOf, plainTextToDoc, } from './PlainTextEditor.js';
|
|
7
7
|
export { tiptap } from './plugin.js';
|
|
8
8
|
export { TiptapEditor } from './react/TiptapEditor.js';
|
|
9
|
-
export {
|
|
10
|
-
export {
|
|
11
|
-
export {
|
|
12
|
-
export { planReplaceBlock, planInsertBlockBefore, planDeleteBlock, planWrapBlocks, planUpdateBlockMark, summarizeBlockStructure, } from './surgicalOps.js';
|
|
9
|
+
export { SuggestionChipExtension, suggestionChipPluginKey, upsertSuggestion, upsertSuggestions, removeSuggestion, remapSuggestions, sortForApproveAll, clampPos, } from './extensions/SuggestionChipExtension.js';
|
|
10
|
+
export { useSuggestionBridge } from './react/useSuggestionBridge.js';
|
|
11
|
+
export { InlineDiffExtension, inlineDiffPluginKey, getInlineDiffState, } from './extensions/InlineDiffExtension.js';
|
|
12
|
+
export { planReplaceBlock, planInsertBlockBefore, planDeleteBlock, planWrapBlocks, planUpdateBlockMark, planReplaceText, summarizeBlockStructure, } from './surgicalOps.js';
|
|
13
13
|
export { renderRichTextToHtml, isRichTextValue, } from './render.js';
|
|
14
14
|
// Default content-block node specs (Intro / FAQ / Alert / Summary / Key takeaways /
|
|
15
15
|
// Pros & cons). `contentBlockNodes` is the exact array `TiptapEditor` registers,
|
|
@@ -1,19 +1,30 @@
|
|
|
1
1
|
import { type NodeViewProps } from '@tiptap/react';
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* summary card with an "Edit" button.
|
|
3
|
+
* React NodeView for the `pilotiqBlock` ProseMirror node. Reads the block
|
|
4
|
+
* type from `node.attrs.blockType`, looks up its `BlockMeta` in
|
|
5
|
+
* `BlockNodeExtension.options.blocks`, and renders a compact summary card.
|
|
7
6
|
*
|
|
8
|
-
* Editing
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
7
|
+
* Editing is **inline** (accordion): clicking the card (or the Edit chevron)
|
|
8
|
+
* expands a panel below the summary that hosts the block's `Block.schema([…])`
|
|
9
|
+
* as a real pilotiq form via `<FormFields>`. Edits write straight back onto
|
|
10
|
+
* the node with `updateAttributes({ blockData })` on every change — the
|
|
11
|
+
* NodeView already owns the node, so there's no host bridge / side panel /
|
|
12
|
+
* position-remapping to thread through.
|
|
14
13
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
14
|
+
* The form is rendered in a `contentEditable={false}` region and every input
|
|
15
|
+
* event is stopped from bubbling into ProseMirror, so the editor never treats
|
|
16
|
+
* the form inputs as document content or hijacks their focus/selection.
|
|
17
|
+
*
|
|
18
|
+
* Reads: each field's `defaultValue` is overridden from the block's stored
|
|
19
|
+
* `blockData`, snapshotted once per expand into `initialValuesRef`. Inputs are
|
|
20
|
+
* uncontrolled (outside a `FormStateProvider`, pilotiq's renderers fall back to
|
|
21
|
+
* `defaultValue`), so write-back transactions re-rendering the NodeView never
|
|
22
|
+
* reset the user's in-progress typing.
|
|
23
|
+
*
|
|
24
|
+
* Writes: container-level `onInput` / `onChange` delegation. Every change
|
|
25
|
+
* snapshots the whole form via `new FormData(formEl)` → `parseFormDataToNested`
|
|
26
|
+
* (rebuilds nested arrays/objects from dotted-path inputs like `items.0.title`)
|
|
27
|
+
* → `coerceBlockValues` (per-fieldType JSON parse / boolean / number coerce).
|
|
17
28
|
*/
|
|
18
29
|
export declare function BlockNodeView(props: NodeViewProps): import("react").JSX.Element | null;
|
|
19
30
|
//# sourceMappingURL=BlockNodeView.d.ts.map
|
|
@@ -1,33 +1,52 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect } from 'react';
|
|
2
|
+
import { useEffect, useRef, useState } from 'react';
|
|
3
3
|
import { NodeViewWrapper } from '@tiptap/react';
|
|
4
|
+
import { FormFields, parseFormDataToNested } from '@pilotiq/pilotiq/react';
|
|
5
|
+
import { coerceBlockValues } from './blockValues.js';
|
|
4
6
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* summary card with an "Edit" button.
|
|
7
|
+
* React NodeView for the `pilotiqBlock` ProseMirror node. Reads the block
|
|
8
|
+
* type from `node.attrs.blockType`, looks up its `BlockMeta` in
|
|
9
|
+
* `BlockNodeExtension.options.blocks`, and renders a compact summary card.
|
|
9
10
|
*
|
|
10
|
-
* Editing
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
11
|
+
* Editing is **inline** (accordion): clicking the card (or the Edit chevron)
|
|
12
|
+
* expands a panel below the summary that hosts the block's `Block.schema([…])`
|
|
13
|
+
* as a real pilotiq form via `<FormFields>`. Edits write straight back onto
|
|
14
|
+
* the node with `updateAttributes({ blockData })` on every change — the
|
|
15
|
+
* NodeView already owns the node, so there's no host bridge / side panel /
|
|
16
|
+
* position-remapping to thread through.
|
|
16
17
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
18
|
+
* The form is rendered in a `contentEditable={false}` region and every input
|
|
19
|
+
* event is stopped from bubbling into ProseMirror, so the editor never treats
|
|
20
|
+
* the form inputs as document content or hijacks their focus/selection.
|
|
21
|
+
*
|
|
22
|
+
* Reads: each field's `defaultValue` is overridden from the block's stored
|
|
23
|
+
* `blockData`, snapshotted once per expand into `initialValuesRef`. Inputs are
|
|
24
|
+
* uncontrolled (outside a `FormStateProvider`, pilotiq's renderers fall back to
|
|
25
|
+
* `defaultValue`), so write-back transactions re-rendering the NodeView never
|
|
26
|
+
* reset the user's in-progress typing.
|
|
27
|
+
*
|
|
28
|
+
* Writes: container-level `onInput` / `onChange` delegation. Every change
|
|
29
|
+
* snapshots the whole form via `new FormData(formEl)` → `parseFormDataToNested`
|
|
30
|
+
* (rebuilds nested arrays/objects from dotted-path inputs like `items.0.title`)
|
|
31
|
+
* → `coerceBlockValues` (per-fieldType JSON parse / boolean / number coerce).
|
|
19
32
|
*/
|
|
20
33
|
export function BlockNodeView(props) {
|
|
21
|
-
const { editor, node,
|
|
34
|
+
const { editor, node, deleteNode, updateAttributes } = props;
|
|
22
35
|
const blockType = String(node.attrs['blockType'] ?? '');
|
|
23
36
|
const blockData = node.attrs['blockData'] ?? {};
|
|
37
|
+
const editable = editor.isEditable;
|
|
24
38
|
// Tiptap mounts NodeViews in a separate React tree, so we can't read the
|
|
25
39
|
// block registry through context. Pull it off the extension's options
|
|
26
40
|
// instead — set by RichTextField via BlockNodeExtension.configure({ blocks }).
|
|
27
41
|
const blockExt = editor.extensionManager.extensions.find((e) => e.name === 'pilotiqBlock');
|
|
28
42
|
const blocks = blockExt?.options['blocks'] ?? [];
|
|
29
|
-
const onEdit = blockExt?.options['onEdit'];
|
|
30
43
|
const meta = blocks.find((b) => b.name === blockType);
|
|
44
|
+
const [expanded, setExpanded] = useState(false);
|
|
45
|
+
// Seeds the form's `defaultValue`s. Re-snapshotted from the live node each
|
|
46
|
+
// time the panel opens; not updated mid-edit (uncontrolled inputs hold their
|
|
47
|
+
// own state while open).
|
|
48
|
+
const initialValuesRef = useRef(blockData);
|
|
49
|
+
const formRef = useRef(null);
|
|
31
50
|
// Self-heal: a block with no `blockType` is malformed — almost always
|
|
32
51
|
// means a stale node from a prior buggy insert. Delete it on mount so
|
|
33
52
|
// the editor doesn't get stuck in an unrecoverable state.
|
|
@@ -47,13 +66,28 @@ export function BlockNodeView(props) {
|
|
|
47
66
|
})
|
|
48
67
|
.filter(Boolean)
|
|
49
68
|
.join(' · ') || meta.label;
|
|
50
|
-
const
|
|
51
|
-
if (!
|
|
69
|
+
const toggleExpanded = () => {
|
|
70
|
+
if (!editable)
|
|
52
71
|
return;
|
|
53
|
-
|
|
54
|
-
|
|
72
|
+
setExpanded((prev) => {
|
|
73
|
+
const next = !prev;
|
|
74
|
+
if (next) {
|
|
75
|
+
initialValuesRef.current =
|
|
76
|
+
node.attrs['blockData'] ?? {};
|
|
77
|
+
}
|
|
78
|
+
return next;
|
|
79
|
+
});
|
|
80
|
+
};
|
|
81
|
+
const handleChange = () => {
|
|
82
|
+
const formEl = formRef.current;
|
|
83
|
+
if (!formEl)
|
|
55
84
|
return;
|
|
56
|
-
|
|
85
|
+
const raw = parseFormDataToNested(new FormData(formEl));
|
|
86
|
+
const coerced = coerceBlockValues(raw, meta.schema);
|
|
87
|
+
updateAttributes({ blockData: coerced });
|
|
57
88
|
};
|
|
58
|
-
return (
|
|
89
|
+
return (_jsxs(NodeViewWrapper, { className: "pilotiq-block my-3 rounded-lg border bg-muted/30", children: [_jsxs("div", { className: "flex items-start justify-between gap-2 px-3 py-2", children: [_jsxs("button", { type: "button", onClick: toggleExpanded, disabled: !editable, className: "flex min-w-0 items-center gap-2 text-left text-sm disabled:cursor-default", children: [meta.icon && _jsx("span", { "aria-hidden": "true", children: meta.icon }), _jsx("span", { className: "font-medium", children: meta.label }), _jsx("span", { className: "line-clamp-1 text-xs text-muted-foreground", children: summary })] }), editable && (_jsxs("div", { className: "flex shrink-0 items-center gap-2", children: [_jsxs("button", { type: "button", onClick: toggleExpanded, "aria-expanded": expanded, className: "flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground", children: [expanded ? 'Done' : 'Edit', _jsx("svg", { viewBox: "0 0 24 24", className: 'size-3.5 transition-transform ' + (expanded ? 'rotate-180' : ''), fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, children: _jsx("path", { d: "m6 9 6 6 6-6" }) })] }), _jsx("button", { type: "button", onClick: () => deleteNode(), className: "text-xs text-destructive hover:underline", children: "Remove" })] }))] }), expanded && editable && (
|
|
90
|
+
// contentEditable=false + event guards keep ProseMirror from treating
|
|
91
|
+
// the form inputs as document content or stealing their focus/caret.
|
|
92
|
+
_jsx("div", { contentEditable: false, className: "border-t px-3 py-3", onMouseDown: (e) => e.stopPropagation(), onPointerDown: (e) => e.stopPropagation(), onKeyDown: (e) => e.stopPropagation(), onKeyUp: (e) => e.stopPropagation(), onPaste: (e) => e.stopPropagation(), onDrop: (e) => e.stopPropagation(), children: _jsx("form", { ref: formRef, onInput: handleChange, onChange: handleChange, onSubmit: (e) => e.preventDefault(), className: "flex flex-col gap-3", children: _jsx(FormFields, { elements: meta.schema, values: initialValuesRef.current }) }) }))] }));
|
|
59
93
|
}
|
|
@@ -5,11 +5,11 @@ import { Slice } from '@tiptap/pm/model';
|
|
|
5
5
|
import { useCollabRoom, getCollabExtensions, } from '@pilotiq/pilotiq/react';
|
|
6
6
|
import { useCollabSeed } from '@rudderjs/sync/react';
|
|
7
7
|
import { createPlainTextEditor, plainTextOf, plainTextToDoc } from '../PlainTextEditor.js';
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
8
|
+
import { SuggestionChipExtension } from '../extensions/SuggestionChipExtension.js';
|
|
9
|
+
import { InlineDiffExtension } from '../extensions/InlineDiffExtension.js';
|
|
10
|
+
import { useSuggestionBridge } from './useSuggestionBridge.js';
|
|
11
|
+
import { useInlineDiff, useIsInlineDiffActive, readDiffViewMarker } from './useInlineDiff.js';
|
|
12
|
+
import { SuggestionBanner } from './SuggestionBanner.js';
|
|
13
13
|
/**
|
|
14
14
|
* Tiptap-backed plain-text editor for pilotiq's `TextField` / `TextareaField`
|
|
15
15
|
* / similar single-line / multi-line text fields when collab is on.
|
|
@@ -100,11 +100,11 @@ export function CollabTextRenderer({ name, fragmentKey, multiline, defaultValue,
|
|
|
100
100
|
// AI suggestions — chip extension (producer-supplied range
|
|
101
101
|
// suggestions) + inline-diff extension (whole-field suggestions:
|
|
102
102
|
// red strikethrough on removed runs, green on inserted, with the
|
|
103
|
-
// `<
|
|
103
|
+
// `<SuggestionBanner>` Accept / Reject below). Both idle until
|
|
104
104
|
// a suggestion arrives via the bridges below. Matches the
|
|
105
105
|
// `TiptapEditor` wiring so the review surface reads identically
|
|
106
106
|
// across RichTextField / MarkdownField / TextField+TextareaField.
|
|
107
|
-
extensions: [...collabExtensions,
|
|
107
|
+
extensions: [...collabExtensions, SuggestionChipExtension, InlineDiffExtension],
|
|
108
108
|
onUpdate: (text) => onChange(text),
|
|
109
109
|
...(onSubmit ? { onSubmit: () => { onSubmit(); return false; } } : {}),
|
|
110
110
|
...(className || editorAttributes
|
|
@@ -128,11 +128,11 @@ export function CollabTextRenderer({ name, fragmentKey, multiline, defaultValue,
|
|
|
128
128
|
editor.setEditable(!disabled);
|
|
129
129
|
}, [editor, disabled]);
|
|
130
130
|
// Cross-package suggestion bridge — sync the host's
|
|
131
|
-
// `<PendingSuggestionsContext>` queue with the editor's `
|
|
131
|
+
// `<PendingSuggestionsContext>` queue with the editor's `InlineSuggestion`
|
|
132
132
|
// extension. No-op when no provider is mounted (default no-op context).
|
|
133
133
|
//
|
|
134
134
|
// Whole-field suggestions do NOT synthesize a chip range anymore —
|
|
135
|
-
// they render through `
|
|
135
|
+
// they render through `useInlineDiff` below (same red/green inline
|
|
136
136
|
// diff + banner as `TiptapEditor`), replacing the old green-pill chip
|
|
137
137
|
// that read differently from the rich-text surface. The bridge stays
|
|
138
138
|
// mounted for producer-supplied `meta.editorRange` suggestions (precise
|
|
@@ -143,12 +143,12 @@ export function CollabTextRenderer({ name, fragmentKey, multiline, defaultValue,
|
|
|
143
143
|
return;
|
|
144
144
|
editor.commands.setContent(plainTextToDoc(value, !!multiline));
|
|
145
145
|
};
|
|
146
|
-
|
|
146
|
+
useSuggestionBridge(editor ?? null, name, {
|
|
147
147
|
onApplyWholeField: applyWholeField,
|
|
148
148
|
});
|
|
149
149
|
// Inline diff for whole-field suggestions — plain-text shape: each
|
|
150
150
|
// line wraps in a `paragraph` node, mirroring `plainTextToDoc`.
|
|
151
|
-
|
|
151
|
+
useInlineDiff(editor ?? null, name, {
|
|
152
152
|
parseSuggestion: (ed, value) => {
|
|
153
153
|
try {
|
|
154
154
|
const node = ed.schema.nodeFromJSON(plainTextToDoc(value, !!multiline));
|
|
@@ -158,9 +158,9 @@ export function CollabTextRenderer({ name, fragmentKey, multiline, defaultValue,
|
|
|
158
158
|
return null;
|
|
159
159
|
}
|
|
160
160
|
},
|
|
161
|
-
resolveDisplayMode: () =>
|
|
161
|
+
resolveDisplayMode: () => readDiffViewMarker(name),
|
|
162
162
|
});
|
|
163
|
-
const isDiffActive =
|
|
163
|
+
const isDiffActive = useIsInlineDiffActive(editor ?? null);
|
|
164
164
|
// First-load seed when collab is active. Collaboration starts the editor
|
|
165
165
|
// empty regardless of `defaultValue`; once the room's first sync
|
|
166
166
|
// resolves, `useCollabSeed` runs the callback inside `ydoc.transact`.
|
|
@@ -216,10 +216,10 @@ export function CollabTextRenderer({ name, fragmentKey, multiline, defaultValue,
|
|
|
216
216
|
// Banner mounts below the editor exactly like `TiptapEditor`'s — it
|
|
217
217
|
// renders nothing while no suggestion is pending for this field, so
|
|
218
218
|
// the single-line text surface keeps its normal footprint.
|
|
219
|
-
return (_jsxs(_Fragment, { children: [_jsx(EditorContent, { editor: editor }), _jsx(
|
|
219
|
+
return (_jsxs(_Fragment, { children: [_jsx(EditorContent, { editor: editor }), _jsx(SuggestionBanner, { fieldName: name, onApplyWholeField: applyWholeField, ...(isDiffActive && editor
|
|
220
220
|
? {
|
|
221
|
-
onAcceptViaEditor: () => editor.commands.
|
|
222
|
-
onRejectViaEditor: () => editor.commands.
|
|
221
|
+
onAcceptViaEditor: () => editor.commands.acceptInlineDiff(),
|
|
222
|
+
onRejectViaEditor: () => editor.commands.rejectInlineDiff(),
|
|
223
223
|
}
|
|
224
224
|
: {}) })] }));
|
|
225
225
|
}
|