@pilotiq/tiptap 3.10.4 → 3.10.6
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 +745 -0
- package/boost/guidelines.md +268 -0
- package/boost/skills/pilotiq-tiptap-blocks/SKILL.md +48 -0
- package/boost/skills/pilotiq-tiptap-blocks/rules/custom-blocks.md +90 -0
- package/boost/skills/pilotiq-tiptap-blocks/rules/slash-menu-and-mentions.md +101 -0
- package/boost/skills/pilotiq-tiptap-blocks/rules/toolbar-and-extensibility.md +161 -0
- package/dist/react/CollabTextRenderer.d.ts.map +1 -1
- package/dist/react/CollabTextRenderer.js +4 -4
- package/dist/react/CollabTextRenderer.js.map +1 -1
- package/dist/react/MarkdownEditor.d.ts.map +1 -1
- package/dist/react/MarkdownEditor.js +4 -5
- package/dist/react/MarkdownEditor.js.map +1 -1
- package/dist/react/TiptapEditor.d.ts.map +1 -1
- package/dist/react/TiptapEditor.js +8 -7
- package/dist/react/TiptapEditor.js.map +1 -1
- package/package.json +6 -3
- package/dist/collabShapes.d.ts +0 -22
- package/dist/collabShapes.d.ts.map +0 -1
- package/dist/collabShapes.js +0 -2
- package/dist/collabShapes.js.map +0 -1
- package/src/Block.ts +0 -75
- package/src/MentionProvider.ts +0 -153
- package/src/PlainTextEditor.dom.test.ts +0 -111
- package/src/PlainTextEditor.test.ts +0 -158
- package/src/PlainTextEditor.ts +0 -229
- package/src/RichTextField.test.ts +0 -447
- package/src/RichTextField.ts +0 -508
- package/src/collabShapes.ts +0 -22
- package/src/extensions/AiInlineDiffExtension.ts +0 -286
- package/src/extensions/AiSuggestionExtension.test.ts +0 -141
- package/src/extensions/AiSuggestionExtension.ts +0 -522
- package/src/extensions/BlockNodeExtension.ts +0 -134
- package/src/extensions/DragHandleExtension.ts +0 -184
- package/src/extensions/GridExtension.test.ts +0 -31
- package/src/extensions/GridExtension.ts +0 -138
- package/src/extensions/MentionExtension.ts +0 -248
- package/src/extensions/MergeTagExtension.ts +0 -75
- package/src/extensions/SlashCommandExtension.test.ts +0 -147
- package/src/extensions/SlashCommandExtension.ts +0 -332
- package/src/extensions/TextSizeMarks.ts +0 -73
- package/src/index.ts +0 -62
- package/src/markdownExtension.ts +0 -19
- package/src/markdownStorage.ts +0 -49
- package/src/plugin.test.ts +0 -19
- package/src/plugin.ts +0 -26
- package/src/react/AiSuggestionBanner.tsx +0 -185
- package/src/react/BlockNodeView.tsx +0 -99
- package/src/react/BlockSidePanel.dom.test.tsx +0 -38
- package/src/react/BlockSidePanel.test.ts +0 -412
- package/src/react/BlockSidePanel.tsx +0 -451
- package/src/react/CollabTextRenderer.tsx +0 -230
- package/src/react/FloatingToolbar.tsx +0 -304
- package/src/react/MarkdownEditor.tsx +0 -606
- package/src/react/MentionMenu.tsx +0 -120
- package/src/react/Palette.tsx +0 -86
- package/src/react/SlashMenu.tsx +0 -129
- package/src/react/TableFloatingToolbar.tsx +0 -154
- package/src/react/TiptapEditor.dom.test.tsx +0 -112
- package/src/react/TiptapEditor.tsx +0 -776
- package/src/react/Toolbar.tsx +0 -438
- package/src/react/toolbarButtons.tsx +0 -579
- package/src/react/useAiInlineDiff.ts +0 -342
- package/src/react/useAiSuggestionBridge.ts +0 -223
- package/src/register.test.ts +0 -14
- package/src/register.ts +0 -42
- package/src/render.test.ts +0 -745
- package/src/render.ts +0 -480
- package/src/surgicalOps.ts +0 -205
- package/src/test/setup.ts +0 -64
package/src/plugin.test.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { describe, it } from 'node:test'
|
|
2
|
-
import assert from 'node:assert/strict'
|
|
3
|
-
import { getFieldRenderer } from '@pilotiq/pilotiq/react'
|
|
4
|
-
import { Pilotiq } from '@pilotiq/pilotiq'
|
|
5
|
-
import { tiptap } from './plugin.js'
|
|
6
|
-
|
|
7
|
-
describe('tiptap() plugin', () => {
|
|
8
|
-
it('exposes a Pilotiq plugin shape', () => {
|
|
9
|
-
const plugin = tiptap()
|
|
10
|
-
assert.equal(plugin.name, '@pilotiq/tiptap')
|
|
11
|
-
assert.equal(typeof plugin.register, 'function')
|
|
12
|
-
})
|
|
13
|
-
|
|
14
|
-
it('plugins([tiptap()]) wires the richtext field renderer', () => {
|
|
15
|
-
Pilotiq.make('test').plugins([tiptap()])
|
|
16
|
-
const renderer = getFieldRenderer('richtext')
|
|
17
|
-
assert.equal(typeof renderer, 'function')
|
|
18
|
-
})
|
|
19
|
-
})
|
package/src/plugin.ts
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import type { PilotiqPlugin } from '@pilotiq/pilotiq'
|
|
2
|
-
import { registerTiptap } from './register.js'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Pilotiq plugin that registers the Tiptap rich-text renderer.
|
|
6
|
-
*
|
|
7
|
-
* Use with `Pilotiq.make(...).plugins([tiptap()])` instead of calling
|
|
8
|
-
* `registerTiptap()` directly from your app's client entry — the plugin
|
|
9
|
-
* runs through the panel module so it's wired in one place.
|
|
10
|
-
*
|
|
11
|
-
* @example
|
|
12
|
-
* ```ts
|
|
13
|
-
* import { Pilotiq } from '@pilotiq/pilotiq'
|
|
14
|
-
* import { tiptap } from '@pilotiq/tiptap'
|
|
15
|
-
*
|
|
16
|
-
* Pilotiq.make('Admin').plugins([tiptap()])
|
|
17
|
-
* ```
|
|
18
|
-
*/
|
|
19
|
-
export function tiptap(): PilotiqPlugin {
|
|
20
|
-
return {
|
|
21
|
-
name: '@pilotiq/tiptap',
|
|
22
|
-
register() {
|
|
23
|
-
registerTiptap()
|
|
24
|
-
},
|
|
25
|
-
}
|
|
26
|
-
}
|
|
@@ -1,185 +0,0 @@
|
|
|
1
|
-
import { useMemo } from 'react'
|
|
2
|
-
import {
|
|
3
|
-
usePendingSuggestionsForField,
|
|
4
|
-
usePendingSuggestions,
|
|
5
|
-
type PendingSuggestion,
|
|
6
|
-
} from '@pilotiq/pilotiq/react'
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Bottom-of-editor banner UI for whole-field AI suggestions on Tiptap
|
|
10
|
-
* surfaces whose content shape can't survive the inline chip widget's
|
|
11
|
-
* plain-text replace (richtext, markdown). The chip path renders the
|
|
12
|
-
* replacement via `Element.textContent = replacement` which surfaces raw
|
|
13
|
-
* HTML / markdown as literal text — fine for plain `TextField`, ugly for
|
|
14
|
-
* the others.
|
|
15
|
-
*
|
|
16
|
-
* Visible only when at least one pending suggestion targets this field
|
|
17
|
-
* AND lacks `meta.editorRange` (i.e. a whole-field replacement from
|
|
18
|
-
* `update_form_state`'s `set_value` op). Range-anchored suggestions stay
|
|
19
|
-
* on the editor-side chip widget path — those have a precise location
|
|
20
|
-
* the user wants to see in context.
|
|
21
|
-
*
|
|
22
|
-
* Phase 1 ships banner-only ("Changes suggested — Accept / Reject"); no
|
|
23
|
-
* inline diff visualization yet. Phase 2 will replace the banner-only
|
|
24
|
-
* UX with a `prosemirror-changeset`-driven inline diff on the editor's
|
|
25
|
-
* doc itself, with the banner staying as the global Accept-all / Reject
|
|
26
|
-
* control bar. See `[[project_pilotiq_text_field_tiptap_rules]]`.
|
|
27
|
-
*
|
|
28
|
-
* Approve runs the renderer-supplied `onApplyWholeField(value)` callback
|
|
29
|
-
* AND dismisses the suggestion from the queue. Reject just dismisses
|
|
30
|
-
* (no doc mutation). Multiple pending whole-field suggestions on the
|
|
31
|
-
* same field stack — Accept all / Reject all collapse the queue in one
|
|
32
|
-
* pass.
|
|
33
|
-
*/
|
|
34
|
-
export interface AiSuggestionBannerProps {
|
|
35
|
-
/** Field name, matches the suggestion's `fieldName`. */
|
|
36
|
-
fieldName: string
|
|
37
|
-
/**
|
|
38
|
-
* Apply a whole-field suggestion to the underlying editor. Receives the
|
|
39
|
-
* raw `suggestedValue` string from the suggestion. The renderer wires
|
|
40
|
-
* its own content-shape-aware `setContent` here (markdown source for
|
|
41
|
-
* MarkdownEditor, HTML / JSON for TiptapEditor).
|
|
42
|
-
*
|
|
43
|
-
* Skipped when `onAcceptViaEditor` is supplied — that path means the
|
|
44
|
-
* editor already holds the proposed state via `AiInlineDiffExtension`,
|
|
45
|
-
* and Accept routes through `acceptAiInlineDiff()` instead. The host
|
|
46
|
-
* still calls `pendingSuggestions.approve(id)` afterwards to dismiss
|
|
47
|
-
* the queue entry.
|
|
48
|
-
*/
|
|
49
|
-
onApplyWholeField: (suggestedValue: string) => void
|
|
50
|
-
/**
|
|
51
|
-
* Diff-aware Accept hook. When supplied, the banner calls this first
|
|
52
|
-
* (so the editor commits its diff state) and then dismisses via the
|
|
53
|
-
* context. `onApplyWholeField` is NOT called in this mode — the
|
|
54
|
-
* editor's current doc is already the accepted state.
|
|
55
|
-
*
|
|
56
|
-
* Sparse so the simple banner path (Phase 1, no diff) keeps its
|
|
57
|
-
* existing semantics.
|
|
58
|
-
*/
|
|
59
|
-
onAcceptViaEditor?: () => void
|
|
60
|
-
/**
|
|
61
|
-
* Diff-aware Reject hook. When supplied, the banner calls this first
|
|
62
|
-
* (so the editor reverts to the baseline) and then dismisses via the
|
|
63
|
-
* context. Sparse — see `onAcceptViaEditor`.
|
|
64
|
-
*/
|
|
65
|
-
onRejectViaEditor?: () => void
|
|
66
|
-
/** Optional class on the outer banner element. Defaults to a minimal styled chrome. */
|
|
67
|
-
className?: string
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Hook variant — returns banner state without rendering, for renderers
|
|
72
|
-
* that want to compose their own chrome. Renderer-agnostic.
|
|
73
|
-
*/
|
|
74
|
-
export function useAiSuggestionBanner(fieldName: string): {
|
|
75
|
-
pending: readonly PendingSuggestion[]
|
|
76
|
-
approveAll: (apply: (value: string) => void) => void
|
|
77
|
-
rejectAll: () => void
|
|
78
|
-
} {
|
|
79
|
-
const { list, dismiss } = usePendingSuggestionsForField(fieldName)
|
|
80
|
-
|
|
81
|
-
// Only whole-field suggestions land in the banner. Range-anchored ones
|
|
82
|
-
// ride the editor chip widget.
|
|
83
|
-
const pending = useMemo(
|
|
84
|
-
() => list.filter(s => !hasEditorRange(s)),
|
|
85
|
-
[list],
|
|
86
|
-
)
|
|
87
|
-
|
|
88
|
-
const approveAll = (apply: (value: string) => void): void => {
|
|
89
|
-
for (const s of pending) {
|
|
90
|
-
if (typeof s.suggestedValue === 'string') apply(s.suggestedValue)
|
|
91
|
-
dismiss(s.id)
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const rejectAll = (): void => {
|
|
96
|
-
for (const s of pending) dismiss(s.id)
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
return { pending, approveAll, rejectAll }
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function hasEditorRange(s: PendingSuggestion): boolean {
|
|
103
|
-
const meta = (s.meta ?? {}) as Record<string, unknown>
|
|
104
|
-
const range = meta['editorRange'] as { from?: unknown; to?: unknown } | undefined
|
|
105
|
-
return !!(range && typeof range.from === 'number' && typeof range.to === 'number')
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export function AiSuggestionBanner({
|
|
109
|
-
fieldName,
|
|
110
|
-
onApplyWholeField,
|
|
111
|
-
onAcceptViaEditor,
|
|
112
|
-
onRejectViaEditor,
|
|
113
|
-
className,
|
|
114
|
-
}: AiSuggestionBannerProps): React.ReactElement | null {
|
|
115
|
-
const { pending, approveAll, rejectAll } = useAiSuggestionBanner(fieldName)
|
|
116
|
-
const { dismiss } = usePendingSuggestions()
|
|
117
|
-
|
|
118
|
-
if (pending.length === 0) return null
|
|
119
|
-
|
|
120
|
-
// First (and usually only) pending suggestion drives the agent-label
|
|
121
|
-
// display. Multiple-at-once is rare in practice — the banner shows the
|
|
122
|
-
// most recent producer to keep the chrome compact.
|
|
123
|
-
const head = pending[0]!
|
|
124
|
-
const sourceLabel = head.source?.agentLabel ?? null
|
|
125
|
-
|
|
126
|
-
const handleAccept = (): void => {
|
|
127
|
-
// Diff-active path — editor's current doc IS the accepted state.
|
|
128
|
-
// Commit via the editor command, then drop the queue entries.
|
|
129
|
-
if (onAcceptViaEditor) {
|
|
130
|
-
onAcceptViaEditor()
|
|
131
|
-
for (const s of pending) dismiss(s.id)
|
|
132
|
-
return
|
|
133
|
-
}
|
|
134
|
-
approveAll(onApplyWholeField)
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const handleReject = (): void => {
|
|
138
|
-
// Diff-active path — editor still holds the proposed state; revert
|
|
139
|
-
// to the captured baseline before dismissing.
|
|
140
|
-
if (onRejectViaEditor) {
|
|
141
|
-
onRejectViaEditor()
|
|
142
|
-
for (const s of pending) dismiss(s.id)
|
|
143
|
-
return
|
|
144
|
-
}
|
|
145
|
-
rejectAll()
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// Per-suggestion controls when there's more than one — keeps the UX
|
|
149
|
-
// discoverable. Single suggestion: Accept / Reject only.
|
|
150
|
-
const single = pending.length === 1
|
|
151
|
-
|
|
152
|
-
return (
|
|
153
|
-
<div
|
|
154
|
-
role="region"
|
|
155
|
-
aria-label="AI suggested changes"
|
|
156
|
-
data-pilotiq-ai-banner=""
|
|
157
|
-
className={className ?? 'pilotiq-ai-banner'}
|
|
158
|
-
>
|
|
159
|
-
<span className="pilotiq-ai-banner-icon" aria-hidden="true">💡</span>
|
|
160
|
-
<span className="pilotiq-ai-banner-label">
|
|
161
|
-
{single
|
|
162
|
-
? sourceLabel
|
|
163
|
-
? `Changes suggested by ${sourceLabel}`
|
|
164
|
-
: 'Changes suggested'
|
|
165
|
-
: `${pending.length} changes suggested`}
|
|
166
|
-
</span>
|
|
167
|
-
<div className="pilotiq-ai-banner-actions">
|
|
168
|
-
<button
|
|
169
|
-
type="button"
|
|
170
|
-
className="pilotiq-ai-banner-reject"
|
|
171
|
-
onClick={handleReject}
|
|
172
|
-
>
|
|
173
|
-
{single ? 'Reject' : 'Reject all'}
|
|
174
|
-
</button>
|
|
175
|
-
<button
|
|
176
|
-
type="button"
|
|
177
|
-
className="pilotiq-ai-banner-accept"
|
|
178
|
-
onClick={handleAccept}
|
|
179
|
-
>
|
|
180
|
-
{single ? 'Accept' : 'Accept all'}
|
|
181
|
-
</button>
|
|
182
|
-
</div>
|
|
183
|
-
</div>
|
|
184
|
-
)
|
|
185
|
-
}
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
import { useEffect } from 'react'
|
|
2
|
-
import { NodeViewWrapper, type NodeViewProps } from '@tiptap/react'
|
|
3
|
-
import type { BlockMeta } from '../Block.js'
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Generic React NodeView for the `pilotiqBlock` ProseMirror node. Reads
|
|
7
|
-
* the block type from `node.attrs.blockType`, looks up its `BlockMeta`
|
|
8
|
-
* in `BlockNodeExtension.options.blocks`, and renders a compact inline
|
|
9
|
-
* summary card with an "Edit" button.
|
|
10
|
-
*
|
|
11
|
-
* Editing happens in a side panel hosted by `TiptapEditor`, NOT inline.
|
|
12
|
-
* The NodeView fires `BlockNodeExtension.options.onEdit(getPos())` when
|
|
13
|
-
* the Edit button is clicked; the host opens its panel anchored to the
|
|
14
|
-
* editor wrapper. NodeViews live in a separate React tree from the host
|
|
15
|
-
* editor, so the bridge has to go through extension options — context
|
|
16
|
-
* doesn't cross trees.
|
|
17
|
-
*
|
|
18
|
-
* If no `onEdit` is wired (e.g. a consumer that uses `BlockNodeExtension`
|
|
19
|
-
* standalone without `TiptapEditor`'s panel), the Edit button is hidden.
|
|
20
|
-
*/
|
|
21
|
-
export function BlockNodeView(props: NodeViewProps) {
|
|
22
|
-
const { editor, node, getPos, deleteNode } = props
|
|
23
|
-
const blockType = String(node.attrs['blockType'] ?? '')
|
|
24
|
-
const blockData = (node.attrs['blockData'] as Record<string, unknown> | undefined) ?? {}
|
|
25
|
-
|
|
26
|
-
// Tiptap mounts NodeViews in a separate React tree, so we can't read the
|
|
27
|
-
// block registry through context. Pull it off the extension's options
|
|
28
|
-
// instead — set by RichTextField via BlockNodeExtension.configure({ blocks }).
|
|
29
|
-
const blockExt = editor.extensionManager.extensions.find((e) => e.name === 'pilotiqBlock')
|
|
30
|
-
const blocks = (blockExt?.options['blocks'] as BlockMeta[] | undefined) ?? []
|
|
31
|
-
const onEdit = blockExt?.options['onEdit'] as ((pos: number) => void) | undefined
|
|
32
|
-
const meta = blocks.find((b) => b.name === blockType)
|
|
33
|
-
|
|
34
|
-
// Self-heal: a block with no `blockType` is malformed — almost always
|
|
35
|
-
// means a stale node from a prior buggy insert. Delete it on mount so
|
|
36
|
-
// the editor doesn't get stuck in an unrecoverable state.
|
|
37
|
-
useEffect(() => {
|
|
38
|
-
if (blockType === '') deleteNode()
|
|
39
|
-
}, [blockType, deleteNode])
|
|
40
|
-
|
|
41
|
-
if (!meta) {
|
|
42
|
-
if (blockType === '') return null
|
|
43
|
-
return (
|
|
44
|
-
<NodeViewWrapper className="my-2 rounded-md border border-destructive/40 bg-destructive/5 p-3 text-sm text-destructive">
|
|
45
|
-
Unknown block type: <code>{blockType}</code>
|
|
46
|
-
</NodeViewWrapper>
|
|
47
|
-
)
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const summary = meta.schema
|
|
51
|
-
.map((f) => {
|
|
52
|
-
const v = blockData[f.name]
|
|
53
|
-
return typeof v === 'string' && v ? v : ''
|
|
54
|
-
})
|
|
55
|
-
.filter(Boolean)
|
|
56
|
-
.join(' · ') || meta.label
|
|
57
|
-
|
|
58
|
-
const handleEdit = (): void => {
|
|
59
|
-
if (!onEdit) return
|
|
60
|
-
const pos = getPos()
|
|
61
|
-
if (typeof pos !== 'number') return
|
|
62
|
-
onEdit(pos)
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
return (
|
|
66
|
-
<NodeViewWrapper className="pilotiq-block my-3 rounded-lg border bg-muted/30">
|
|
67
|
-
<div className="flex items-start justify-between gap-2 px-3 py-2">
|
|
68
|
-
<button
|
|
69
|
-
type="button"
|
|
70
|
-
onClick={handleEdit}
|
|
71
|
-
disabled={!onEdit}
|
|
72
|
-
className="flex items-center gap-2 text-left text-sm disabled:cursor-default"
|
|
73
|
-
>
|
|
74
|
-
{meta.icon && <span aria-hidden="true">{meta.icon}</span>}
|
|
75
|
-
<span className="font-medium">{meta.label}</span>
|
|
76
|
-
<span className="text-xs text-muted-foreground line-clamp-1">{summary}</span>
|
|
77
|
-
</button>
|
|
78
|
-
<div className="flex items-center gap-2">
|
|
79
|
-
{onEdit && (
|
|
80
|
-
<button
|
|
81
|
-
type="button"
|
|
82
|
-
onClick={handleEdit}
|
|
83
|
-
className="text-xs text-muted-foreground hover:text-foreground"
|
|
84
|
-
>
|
|
85
|
-
Edit
|
|
86
|
-
</button>
|
|
87
|
-
)}
|
|
88
|
-
<button
|
|
89
|
-
type="button"
|
|
90
|
-
onClick={() => deleteNode()}
|
|
91
|
-
className="text-xs text-destructive hover:underline"
|
|
92
|
-
>
|
|
93
|
-
Remove
|
|
94
|
-
</button>
|
|
95
|
-
</div>
|
|
96
|
-
</div>
|
|
97
|
-
</NodeViewWrapper>
|
|
98
|
-
)
|
|
99
|
-
}
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import { describe, it } from 'node:test'
|
|
2
|
-
import assert from 'node:assert/strict'
|
|
3
|
-
import React from 'react'
|
|
4
|
-
import { render, cleanup, screen } from '@testing-library/react'
|
|
5
|
-
|
|
6
|
-
import { clampPanelWidth } from './BlockSidePanel.js'
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Phase 6e proof-of-concept — exercise React Testing Library's
|
|
10
|
-
* `render()` against the jsdom environment that `src/test/setup.ts`
|
|
11
|
-
* boots. The pure helper `clampPanelWidth` is already covered by the
|
|
12
|
-
* neighbouring `BlockSidePanel.test.ts`; this file proves the RTL
|
|
13
|
-
* surface (render / screen / cleanup) actually works in the test
|
|
14
|
-
* harness — every future component-level test for `BlockSidePanel`,
|
|
15
|
-
* `Toolbar`, `SlashMenu`, etc. uses the same primitives.
|
|
16
|
-
*/
|
|
17
|
-
describe('RTL render() (DOM)', () => {
|
|
18
|
-
it('renders a trivial React tree and queries it via `screen`', () => {
|
|
19
|
-
render(<div data-testid="probe">hello tiptap</div>)
|
|
20
|
-
try {
|
|
21
|
-
const node = screen.getByTestId('probe')
|
|
22
|
-
assert.equal(node.textContent, 'hello tiptap')
|
|
23
|
-
} finally {
|
|
24
|
-
cleanup()
|
|
25
|
-
}
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
it('exports clampPanelWidth as a stable pure helper (sanity)', () => {
|
|
29
|
-
// Cross-check: pure-data assertions still work alongside RTL
|
|
30
|
-
// mounts. `clampPanelWidth` is the helper tested via the
|
|
31
|
-
// pure-mode `BlockSidePanel.test.ts` already — re-asserting one
|
|
32
|
-
// case here confirms the dual-import surface (both pure tests
|
|
33
|
-
// and DOM tests in the same package) doesn't conflict.
|
|
34
|
-
assert.equal(clampPanelWidth(100), 240)
|
|
35
|
-
assert.equal(clampPanelWidth(320), 320)
|
|
36
|
-
assert.equal(clampPanelWidth(1000), 600)
|
|
37
|
-
})
|
|
38
|
-
})
|