@slexkit/tiptap 0.3.3 → 0.5.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/README.md CHANGED
@@ -1,88 +1,45 @@
1
1
  # @slexkit/tiptap
2
2
 
3
- Tiptap extension for explicit `slex` fenced UI blocks.
4
-
5
- ```sh
6
- npm install slexkit @slexkit/theme-shadcn @slexkit/tiptap @tiptap/core @tiptap/pm @tiptap/starter-kit @tiptap/extension-code-block @tiptap/markdown
7
- ```
8
-
9
- Import the SlexKit theme once in your app entry:
3
+ Official Tiptap code-block node view for HTML-first `slex` fences.
10
4
 
11
5
  ```ts
12
- import "@slexkit/theme-shadcn/style.css";
13
- import "@slexkit/tiptap/style.css";
14
- ```
15
-
16
- ## Minimal setup
17
-
18
- ```ts
19
- import { Editor } from "@tiptap/core";
20
6
  import StarterKit from "@tiptap/starter-kit";
21
7
  import { createSlexKitTiptapExtension } from "@slexkit/tiptap";
8
+ import { createSlexKitMarkdownRuntimeHost } from "slexkit";
22
9
 
23
- const editor = new Editor({
24
- element: document.querySelector("#editor"),
25
- extensions: [
26
- StarterKit.configure({ codeBlock: false }),
27
- createSlexKitTiptapExtension({
28
- artifactId: "note-42",
29
- }),
30
- ],
31
- });
32
- ```
33
-
34
- Only code blocks whose language is exactly `slex` are rendered by SlexKit. Other code blocks stay ordinary Tiptap code blocks.
35
-
36
- ## Markdown roundtrip
37
-
38
- Add Tiptap's Markdown extension when loading or exporting Markdown:
39
-
40
- ```ts
41
- import { Markdown } from "@tiptap/markdown";
42
-
43
- const editor = new Editor({
44
- extensions: [
45
- StarterKit.configure({ codeBlock: false }),
46
- Markdown,
47
- createSlexKitTiptapExtension({ artifactId: "doc" }),
48
- ],
49
- content: markdown,
50
- contentType: "markdown",
10
+ const runtimeHost = createSlexKitMarkdownRuntimeHost({
11
+ secureFrame: { runtimeUrl: "/assets/slexkit.runtime.js" },
51
12
  });
52
13
 
53
- const nextMarkdown = editor.getMarkdown();
14
+ const extensions = [
15
+ StarterKit.configure({ codeBlock: false }),
16
+ createSlexKitTiptapExtension({ runtimeHost }),
17
+ ];
54
18
  ```
55
19
 
56
- The adapter uses normal fenced code blocks instead of a custom tokenizer, so ` ```slex ` blocks remain readable in plain Markdown hosts.
57
-
58
- ## Shared state across fences
59
-
60
- All `slex` blocks in one editor share the same artifact runtime by default. A state-only fence can seed later renderable fences:
20
+ The extension replaces only code blocks whose language is exactly `slex`.
21
+ Ordinary code blocks retain normal Tiptap behavior and Markdown round-tripping.
61
22
 
62
23
  ````md
63
- ```slex
64
- {
65
- namespace: "calc",
66
- g: { value: 21 }
67
- }
68
- ```
69
-
70
- ```slex
71
- {
72
- namespace: "calc",
73
- layout: {
74
- "text:answer": { "$text": "'answer: ' + (g.value * 2)" }
75
- }
76
- }
77
- ```
24
+ ~~~slex
25
+ <slex-input id="voltage" type="engineering" label="Voltage" value="12"></slex-input>
26
+ <slex-stat id="power" label="Power" unit="W"></slex-stat>
27
+ <script type="module">
28
+ const voltage = slex.root.querySelector("#voltage");
29
+ const power = slex.root.querySelector("#power");
30
+ const update = () => { power.value = voltage.valueAsNumber * 2; };
31
+ voltage.addEventListener("input", update);
32
+ update();
33
+ </script>
34
+ ~~~
78
35
  ````
79
36
 
80
- ## Runtime boundary
81
-
82
- This adapter defaults to trusted runtime mode, intended for application-authored content, repository-maintained snippets, reviewed examples, and local document workflows. Use SlexKit secure runtime APIs for untrusted user Markdown or direct agent output.
83
-
84
- ## Documentation
37
+ Each editor owns a stable artifact; each node view owns a stable block ID.
38
+ Updates use the runtime host's preview/commit protocol, so invalid source reports
39
+ a diagnostic without destroying the last committed iframe. The adapter never
40
+ interprets DSL objects, namespaces, state-only blocks, expression strings, or
41
+ handler strings.
85
42
 
86
- - [Official Tiptap editor example](https://slexkit.app.simgor.cn/examples/tiptap-host)
87
- - [Runnable example source](https://github.com/slexkit/slexkit/tree/main/examples/tiptap)
88
- - [SlexKit Host integration](https://github.com/slexkit/slexkit/blob/main/site/content/reference/integration/en-US.md)
43
+ Options include `artifactId`, `runtimeHost`, `theme`, `showSource`, labels,
44
+ `className`, and `onError`. Custom legacy hosts remain supported for complete
45
+ commits, though atomic preservation requires `updateArtifact`.
package/dist/index.d.ts CHANGED
@@ -1,11 +1,11 @@
1
1
  import type { Editor } from "@tiptap/core";
2
- import { type SlexKitMarkdownRuntimeHost, type ThemeMode } from "slexkit";
2
+ import { type SlexKitMarkdownRuntimeHost, type HtmlTheme } from "slexkit";
3
3
  export type SlexKitTiptapOptions = {
4
4
  artifactId?: string | ((ctx: {
5
5
  editor: Editor;
6
6
  }) => string);
7
7
  runtimeHost?: SlexKitMarkdownRuntimeHost;
8
- theme?: ThemeMode;
8
+ theme?: HtmlTheme;
9
9
  showSource?: boolean;
10
10
  sourceLabel?: string;
11
11
  previewLabel?: string;
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import CodeBlock from "@tiptap/extension-code-block";
2
- import { createSlexKitMarkdownRuntimeHost, parseSlexSource, } from "slexkit";
2
+ import { createSlexKitMarkdownRuntimeHost, inspectSlexSource, } from "slexkit";
3
3
  const DEFAULT_OPTIONS = {
4
4
  showSource: true,
5
5
  sourceLabel: "Source",
@@ -20,26 +20,31 @@ const PREVIEW_INTERACTIVE_SELECTOR = [
20
20
  "[role='tab']",
21
21
  "[role='textbox']",
22
22
  ].join(",");
23
+ const TRANSIENT_POINTER_INTERACTIVE_SELECTOR = [
24
+ "a[href]",
25
+ "button",
26
+ "input[type='button']",
27
+ "input[type='checkbox']",
28
+ "input[type='image']",
29
+ "input[type='radio']",
30
+ "input[type='range']",
31
+ "input[type='reset']",
32
+ "input[type='submit']",
33
+ "[role='button']",
34
+ "[role='checkbox']",
35
+ "[role='radio']",
36
+ "[role='slider']",
37
+ "[role='switch']",
38
+ "[role='tab']",
39
+ ].join(",");
23
40
  const editorArtifactIds = new WeakMap();
24
41
  let nextArtifactId = 0;
25
42
  const sourceOpenStates = new Map();
26
43
  const editorArtifactRuntimes = new WeakMap();
27
- function isRecord(value) {
28
- return !!value && typeof value === "object" && !Array.isArray(value);
29
- }
30
- function isRenderableSource(value) {
31
- if (!isRecord(value))
32
- return false;
33
- if ("layout" in value)
34
- return isRecord(value.layout) && Object.keys(value.layout).length > 0;
35
- if ("namespace" in value || "g" in value)
36
- return false;
37
- return Object.keys(value).some((key) => key.includes(":"));
38
- }
39
- function isStateOnlySource(value) {
40
- return isRecord(value)
41
- && !isRenderableSource(value)
42
- && ("slex" in value || "namespace" in value || "g" in value);
44
+ function isIncrementalRuntimeHost(host) {
45
+ const candidate = host;
46
+ return typeof candidate.updateArtifact === "function"
47
+ && typeof candidate.resetArtifact === "function";
43
48
  }
44
49
  function resolveArtifactId(editor, option) {
45
50
  if (typeof option === "function")
@@ -82,6 +87,8 @@ function resolveArtifactRuntime(editor, artifactId, runtimeHost) {
82
87
  runtimeHost: runtimeHost ?? createSlexKitMarkdownRuntimeHost(),
83
88
  views: new Set(),
84
89
  scheduled: false,
90
+ pending: Promise.resolve(),
91
+ nextBlockId: 0,
85
92
  };
86
93
  runtimes.set(artifactId, runtime);
87
94
  }
@@ -93,8 +100,36 @@ function scheduleArtifactRender(runtime) {
93
100
  runtime.scheduled = true;
94
101
  queueMicrotask(() => {
95
102
  runtime.scheduled = false;
96
- runtime.runtimeHost.disposeArtifact(runtime.artifactId);
97
- for (const view of Array.from(runtime.views).sort(compareNodeViewOrder)) {
103
+ const views = Array.from(runtime.views).sort(compareNodeViewOrder);
104
+ if (isIncrementalRuntimeHost(runtime.runtimeHost)) {
105
+ const prepared = views.map((view) => view.prepareIncrementalRender());
106
+ const phase = prepared.every((entry) => entry.valid)
107
+ ? "commit"
108
+ : "preview";
109
+ const host = runtime.runtimeHost;
110
+ const update = async () => {
111
+ if (runtime.disposed)
112
+ return;
113
+ try {
114
+ await host.updateArtifact(runtime.artifactId, prepared.map((entry) => entry.block), { phase });
115
+ if (phase === "commit") {
116
+ for (const entry of prepared) {
117
+ entry.view.finishIncrementalRender();
118
+ }
119
+ }
120
+ }
121
+ catch (error) {
122
+ if (phase === "commit") {
123
+ for (const entry of prepared) {
124
+ entry.view.reportIncrementalError(error);
125
+ }
126
+ }
127
+ }
128
+ };
129
+ runtime.pending = runtime.pending.then(update, update);
130
+ return;
131
+ }
132
+ for (const view of views) {
98
133
  view.renderWithRuntime(runtime.runtimeHost);
99
134
  }
100
135
  });
@@ -105,6 +140,7 @@ function unregisterArtifactRuntime(editor, runtime, view) {
105
140
  scheduleArtifactRender(runtime);
106
141
  return;
107
142
  }
143
+ runtime.disposed = true;
108
144
  runtime.runtimeHost.disposeArtifact(runtime.artifactId);
109
145
  editorArtifactRuntimes.get(editor)?.delete(runtime.artifactId);
110
146
  }
@@ -125,8 +161,17 @@ function focusPreviewInteractiveTarget(event) {
125
161
  const target = event.target;
126
162
  const targetElement = target instanceof Element ? target : target instanceof Node ? target.parentElement : null;
127
163
  const interactive = targetElement?.closest(PREVIEW_INTERACTIVE_SELECTOR);
128
- if (interactive instanceof HTMLElement)
129
- interactive.focus({ preventScroll: true });
164
+ if (!(interactive instanceof HTMLElement))
165
+ return null;
166
+ interactive.focus({ preventScroll: true });
167
+ return interactive;
168
+ }
169
+ function shouldRestorePreviewFocusAfterPointer(element) {
170
+ return !!element.closest(TRANSIENT_POINTER_INTERACTIVE_SELECTOR);
171
+ }
172
+ function pointerIdFromEvent(event) {
173
+ const pointerEvent = event;
174
+ return typeof pointerEvent?.pointerId === "number" ? pointerEvent.pointerId : undefined;
130
175
  }
131
176
  function markPreviewInteractive(preview, force = false) {
132
177
  if (preview.getAttribute("contenteditable") !== "false") {
@@ -134,18 +179,38 @@ function markPreviewInteractive(preview, force = false) {
134
179
  }
135
180
  preview.querySelectorAll(PREVIEW_INTERACTIVE_SELECTOR).forEach((element) => {
136
181
  if (element instanceof HTMLElement && (force || element.getAttribute("contenteditable") !== "true")) {
137
- if (force)
182
+ if (force && !element.contains(preview.ownerDocument.activeElement))
138
183
  element.removeAttribute("contenteditable");
139
184
  element.setAttribute("contenteditable", "true");
140
185
  }
141
186
  });
142
187
  }
188
+ const previewMarks = new WeakMap();
189
+ function cancelPreviewInteractiveMark(preview) {
190
+ const pending = previewMarks.get(preview);
191
+ if (!pending)
192
+ return;
193
+ pending.cancelled = true;
194
+ if (pending.timer !== undefined)
195
+ preview.ownerDocument.defaultView?.clearTimeout(pending.timer);
196
+ if (pending.frame !== undefined)
197
+ preview.ownerDocument.defaultView?.cancelAnimationFrame(pending.frame);
198
+ previewMarks.delete(preview);
199
+ }
143
200
  function schedulePreviewInteractiveMark(preview) {
144
201
  markPreviewInteractive(preview);
145
- queueMicrotask(() => markPreviewInteractive(preview, true));
202
+ if (previewMarks.has(preview))
203
+ return;
204
+ const pending = { cancelled: false };
205
+ previewMarks.set(preview, pending);
206
+ const mark = () => { if (!pending.cancelled)
207
+ markPreviewInteractive(preview, true); };
208
+ queueMicrotask(mark);
146
209
  const win = preview.ownerDocument.defaultView;
147
- win?.setTimeout(() => markPreviewInteractive(preview, true), 0);
148
- win?.requestAnimationFrame(() => markPreviewInteractive(preview, true));
210
+ pending.timer = win?.setTimeout(() => { pending.timer = undefined; mark(); if (pending.frame === undefined)
211
+ previewMarks.delete(preview); }, 0);
212
+ pending.frame = win?.requestAnimationFrame(() => { pending.frame = undefined; mark(); if (pending.timer === undefined)
213
+ previewMarks.delete(preview); });
149
214
  }
150
215
  function renderError(error, source, container) {
151
216
  const doc = container.ownerDocument || document;
@@ -192,15 +257,113 @@ class SlexKitCodeBlockNodeView {
192
257
  editor;
193
258
  options;
194
259
  preview;
260
+ diagnostic;
195
261
  runtime;
262
+ blockId;
196
263
  interactiveObserver;
264
+ pendingFocusRestore;
265
+ pointerFocusContext;
266
+ focusTimers = new Set();
267
+ focusFrames = new Set();
268
+ clearKeyboardPointerIntent = () => {
269
+ this.pointerFocusContext = undefined;
270
+ this.pendingFocusRestore = undefined;
271
+ this.removeFocusRestoreListeners();
272
+ this.cancelFocusWork();
273
+ };
274
+ cancelFocusWork() {
275
+ const win = this.preview.ownerDocument.defaultView;
276
+ for (const timer of this.focusTimers)
277
+ win?.clearTimeout(timer);
278
+ for (const frame of this.focusFrames)
279
+ win?.cancelAnimationFrame(frame);
280
+ this.focusTimers.clear();
281
+ this.focusFrames.clear();
282
+ }
197
283
  renderToken = 0;
284
+ focusPreviewPointerTarget = (event) => {
285
+ const restoreEditor = this.editor.isFocused;
286
+ const interactive = focusPreviewInteractiveTarget(event);
287
+ this.pointerFocusContext = { restoreEditor, expiresAt: Date.now() + 500,
288
+ scope: interactive?.closest(".slex-select,.slex-checkbox,.slex-switch,.slex-tabs") ?? interactive ?? undefined };
289
+ if (!interactive || !shouldRestorePreviewFocusAfterPointer(interactive))
290
+ return;
291
+ this.pendingFocusRestore = {
292
+ target: interactive,
293
+ restoreEditor,
294
+ pointerId: typeof event.pointerId === "number" ? event.pointerId : null,
295
+ };
296
+ this.addFocusRestoreListeners();
297
+ };
298
+ clearPointerFocusedPreviewTarget = (event) => {
299
+ const context = this.pointerFocusContext;
300
+ if (!context || Date.now() > context.expiresAt)
301
+ return;
302
+ const target = event.target;
303
+ const targetElement = target instanceof Element ? target : target instanceof Node ? target.parentElement : null;
304
+ const interactive = targetElement?.closest(TRANSIENT_POINTER_INTERACTIVE_SELECTOR);
305
+ if (!(interactive instanceof HTMLElement))
306
+ return;
307
+ if (context.scope && !context.scope.contains(interactive))
308
+ return;
309
+ if (this.pendingFocusRestore?.target === interactive)
310
+ return;
311
+ this.queuePreviewFocusCleanup({
312
+ target: interactive,
313
+ restoreEditor: context.restoreEditor,
314
+ });
315
+ };
316
+ finishPreviewPointerFocus = (event) => {
317
+ const pending = this.pendingFocusRestore;
318
+ if (!pending)
319
+ return;
320
+ const pointerId = pointerIdFromEvent(event);
321
+ if (pointerId !== undefined
322
+ && pending.pointerId !== null
323
+ && pointerId !== pending.pointerId) {
324
+ return;
325
+ }
326
+ this.pendingFocusRestore = undefined;
327
+ this.removeFocusRestoreListeners();
328
+ this.queuePreviewFocusCleanup(pending);
329
+ };
330
+ queuePreviewFocusCleanup(pending) {
331
+ const doc = this.preview.ownerDocument || document;
332
+ const win = doc.defaultView;
333
+ const cleanupFocus = () => {
334
+ if (!this.dom.isConnected)
335
+ return;
336
+ const active = doc.activeElement;
337
+ const clearedTarget = active === pending.target;
338
+ if (clearedTarget)
339
+ pending.target.blur();
340
+ const nextActive = doc.activeElement;
341
+ const canRestoreEditor = pending.restoreEditor
342
+ && !this.editor.isDestroyed
343
+ && (clearedTarget || nextActive === doc.body || nextActive === doc.documentElement);
344
+ if (canRestoreEditor) {
345
+ this.editor.commands.focus(undefined, { scrollIntoView: false });
346
+ }
347
+ };
348
+ if (!win)
349
+ return;
350
+ for (const ms of [0, 50]) {
351
+ const timer = win.setTimeout(() => { this.focusTimers.delete(timer); cleanupFocus(); }, ms);
352
+ this.focusTimers.add(timer);
353
+ }
354
+ const frame = win.requestAnimationFrame(() => { this.focusFrames.delete(frame); cleanupFocus(); });
355
+ this.focusFrames.add(frame);
356
+ }
357
+ cancelPreviewPointerFocus = (event) => {
358
+ this.finishPreviewPointerFocus(event);
359
+ };
198
360
  constructor(node, props, options) {
199
361
  this.node = node;
200
362
  this.options = options;
201
363
  this.editor = props.editor;
202
364
  this.artifactId = resolveArtifactId(props.editor, options.artifactId);
203
365
  this.runtime = resolveArtifactRuntime(props.editor, this.artifactId, options.runtimeHost);
366
+ this.blockId = `tiptap-block-${++this.runtime.nextBlockId}`;
204
367
  this.runtime.views.add(this);
205
368
  const doc = props.view.dom.ownerDocument || document;
206
369
  this.dom = createElement(doc, "div", ["slex-tiptap-block", options.className].filter(Boolean).join(" "));
@@ -210,8 +373,11 @@ class SlexKitCodeBlockNodeView {
210
373
  if (label)
211
374
  label.textContent = options.previewLabel;
212
375
  this.preview = createElement(doc, "div", "slex-tiptap-preview");
376
+ this.diagnostic = createElement(doc, "div", "slex-tiptap-diagnostic");
213
377
  this.preview.setAttribute("contenteditable", "false");
214
- this.preview.addEventListener("pointerdown", focusPreviewInteractiveTarget, { capture: true });
378
+ doc.addEventListener("keydown", this.clearKeyboardPointerIntent, { capture: true });
379
+ this.preview.addEventListener("pointerdown", this.focusPreviewPointerTarget, { capture: true });
380
+ this.preview.addEventListener("focusin", this.clearPointerFocusedPreviewTarget, { capture: true });
215
381
  this.preview.addEventListener("mousedown", focusPreviewInteractiveTarget, { capture: true });
216
382
  this.preview.addEventListener("click", focusPreviewInteractiveTarget, { capture: true });
217
383
  if (typeof MutationObserver !== "undefined") {
@@ -259,13 +425,13 @@ class SlexKitCodeBlockNodeView {
259
425
  sourceToggle.addEventListener("click", stopSourceClick, { capture: true });
260
426
  setSourceOpen(sourceOpen);
261
427
  sourceShell.append(sourceToggle, pre);
262
- this.dom.append(...[label, this.preview, sourceShell].filter((element) => !!element));
428
+ this.dom.append(...[label, this.diagnostic, this.preview, sourceShell].filter((element) => !!element));
263
429
  }
264
430
  else {
265
431
  const sourceShell = createElement(doc, "div", "slex-tiptap-source slex-tiptap-source-hidden");
266
432
  sourceShell.hidden = true;
267
433
  sourceShell.append(pre);
268
- this.dom.append(...[label, this.preview, sourceShell].filter((element) => !!element));
434
+ this.dom.append(...[label, this.diagnostic, this.preview, sourceShell].filter((element) => !!element));
269
435
  }
270
436
  scheduleArtifactRender(this.runtime);
271
437
  }
@@ -277,7 +443,16 @@ class SlexKitCodeBlockNodeView {
277
443
  return true;
278
444
  }
279
445
  destroy() {
446
+ this.renderToken += 1;
447
+ this.cancelFocusWork();
448
+ this.preview.ownerDocument.removeEventListener("keydown", this.clearKeyboardPointerIntent, { capture: true });
449
+ cancelPreviewInteractiveMark(this.preview);
280
450
  this.interactiveObserver?.disconnect();
451
+ this.removeFocusRestoreListeners();
452
+ this.preview.removeEventListener("pointerdown", this.focusPreviewPointerTarget, { capture: true });
453
+ this.preview.removeEventListener("mousedown", focusPreviewInteractiveTarget, { capture: true });
454
+ this.preview.removeEventListener("click", focusPreviewInteractiveTarget, { capture: true });
455
+ this.preview.removeEventListener("focusin", this.clearPointerFocusedPreviewTarget, { capture: true });
281
456
  this.preview.replaceChildren();
282
457
  unregisterArtifactRuntime(this.editor, this.runtime, this);
283
458
  }
@@ -297,6 +472,8 @@ class SlexKitCodeBlockNodeView {
297
472
  const target = mutation.target;
298
473
  if (this.preview.contains(target))
299
474
  return true;
475
+ if (this.diagnostic.contains(target))
476
+ return true;
300
477
  if (target === this.contentDOM || this.contentDOM.contains(target))
301
478
  return false;
302
479
  const targetElement = target instanceof Element ? target : target.parentElement;
@@ -315,41 +492,78 @@ class SlexKitCodeBlockNodeView {
315
492
  scheduleArtifactRender(this.runtime);
316
493
  });
317
494
  }
495
+ addFocusRestoreListeners() {
496
+ const doc = this.preview.ownerDocument || document;
497
+ doc.addEventListener("pointerup", this.finishPreviewPointerFocus, { capture: true });
498
+ doc.addEventListener("pointercancel", this.cancelPreviewPointerFocus, { capture: true });
499
+ doc.defaultView?.addEventListener("blur", this.cancelPreviewPointerFocus);
500
+ }
501
+ removeFocusRestoreListeners() {
502
+ const doc = this.preview.ownerDocument || document;
503
+ doc.removeEventListener("pointerup", this.finishPreviewPointerFocus, { capture: true });
504
+ doc.removeEventListener("pointercancel", this.cancelPreviewPointerFocus, { capture: true });
505
+ doc.defaultView?.removeEventListener("blur", this.cancelPreviewPointerFocus);
506
+ }
318
507
  renderWithRuntime(runtimeHost) {
319
508
  const source = this.source();
320
509
  this.dom.hidden = false;
321
510
  this.preview.hidden = false;
322
- this.preview.replaceChildren();
323
- const parsed = parseSlexSource(source);
324
- if (!parsed.ok) {
325
- renderError(parsed.error, source, this.preview);
326
- this.options.onError?.(parsed.error, source);
511
+ const inspected = inspectSlexSource(source);
512
+ if (!inspected.ok) {
513
+ renderError(inspected.error, source, this.diagnostic);
514
+ this.options.onError?.(inspected.error, source);
327
515
  return;
328
516
  }
329
517
  try {
518
+ runtimeHost.disposeBlock(this.preview);
330
519
  runtimeHost.mountBlock({
331
520
  artifactId: this.artifactId,
521
+ blockId: this.blockId,
332
522
  source,
333
523
  container: this.preview,
334
524
  theme: this.options.theme,
335
525
  });
526
+ this.diagnostic.replaceChildren();
336
527
  schedulePreviewInteractiveMark(this.preview);
337
528
  }
338
529
  catch (error) {
339
- renderError(error, source, this.preview);
530
+ renderError(error, source, this.diagnostic);
340
531
  this.options.onError?.(error, source);
341
532
  return;
342
533
  }
343
- if (isStateOnlySource(parsed.value) && !this.preview.querySelector(".slexkit-root")) {
344
- if (!this.options.showSource) {
345
- this.dom.hidden = true;
346
- this.preview.hidden = true;
347
- return;
348
- }
349
- const placeholder = createElement(this.preview.ownerDocument || document, "div", "slex-tiptap-state");
350
- placeholder.textContent = "State block";
351
- this.preview.append(placeholder);
534
+ }
535
+ prepareIncrementalRender() {
536
+ const source = this.source();
537
+ const inspected = inspectSlexSource(source);
538
+ if (inspected.ok) {
539
+ this.diagnostic.replaceChildren();
352
540
  }
541
+ else {
542
+ renderError(inspected.error, source, this.diagnostic);
543
+ this.options.onError?.(inspected.error, source);
544
+ }
545
+ return {
546
+ view: this,
547
+ block: {
548
+ artifactId: this.artifactId,
549
+ blockId: this.blockId,
550
+ source,
551
+ container: this.preview,
552
+ theme: this.options.theme,
553
+ },
554
+ valid: inspected.ok,
555
+ };
556
+ }
557
+ finishIncrementalRender() {
558
+ this.dom.hidden = false;
559
+ this.preview.hidden = false;
560
+ this.diagnostic.replaceChildren();
561
+ schedulePreviewInteractiveMark(this.preview);
562
+ }
563
+ reportIncrementalError(error) {
564
+ const source = this.source();
565
+ renderError(error, source, this.diagnostic);
566
+ this.options.onError?.(error, source);
353
567
  }
354
568
  }
355
569
  export function createSlexKitTiptapExtension(options = {}) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@slexkit/tiptap",
3
- "version": "0.3.3",
3
+ "version": "0.5.0",
4
4
  "description": "Tiptap extension for SlexKit fenced UI blocks.",
5
5
  "author": "SlexKit contributors",
6
6
  "type": "module",
@@ -48,9 +48,12 @@
48
48
  "@tiptap/extension-code-block": "^3.0.0",
49
49
  "@tiptap/markdown": "^3.0.0",
50
50
  "@tiptap/pm": "^3.0.0",
51
- "slexkit": "^0.3.3"
51
+ "slexkit": "^0.5.0"
52
52
  },
53
53
  "peerDependenciesMeta": {
54
+ "slexkit": {
55
+ "optional": true
56
+ },
54
57
  "@tiptap/markdown": {
55
58
  "optional": true
56
59
  }
@@ -60,7 +63,6 @@
60
63
  "@tiptap/extension-code-block": "^3.27.1",
61
64
  "@tiptap/markdown": "^3.27.1",
62
65
  "@tiptap/pm": "^3.27.1",
63
- "@tiptap/starter-kit": "^3.27.1",
64
- "slexkit": "file:../.."
66
+ "@tiptap/starter-kit": "^3.27.1"
65
67
  }
66
68
  }