testomatio-editor-blocks 0.1.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.
@@ -0,0 +1,379 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { defaultBlockSpecs, defaultProps } from "@blocknote/core";
3
+ import { BlockNoteSchema } from "@blocknote/core";
4
+ import { createReactBlockSpec } from "@blocknote/react";
5
+ import { useCallback, useEffect, useRef, useState } from "react";
6
+ function escapeHtml(text) {
7
+ return text
8
+ .replace(/&/g, "&")
9
+ .replace(/</g, "&lt;")
10
+ .replace(/>/g, "&gt;")
11
+ .replace(/\"/g, "&quot;")
12
+ .replace(/'/g, "&#39;");
13
+ }
14
+ const IMAGE_MARKDOWN_REGEX = /!\[([^\]]*)\]\(([^)]+)\)/g;
15
+ function markdownToHtml(markdown) {
16
+ if (!markdown) {
17
+ return "";
18
+ }
19
+ const lines = markdown.split(/\n/);
20
+ const htmlLines = lines.map((line) => {
21
+ const inline = parseInlineMarkdown(line);
22
+ const html = inlineToHtml(inline);
23
+ if (!html) {
24
+ return html;
25
+ }
26
+ return html.replace(IMAGE_MARKDOWN_REGEX, (_match, alt = "", src = "") => `<img src="${escapeHtml(src)}" alt="${escapeHtml(alt)}" class="bn-inline-image" contenteditable="false" draggable="false" />`);
27
+ });
28
+ return htmlLines.join("<br />");
29
+ }
30
+ function parseInlineMarkdown(text) {
31
+ if (!text) {
32
+ return [];
33
+ }
34
+ const normalized = text.replace(/\\([*_`~])/g, "\uE000$1");
35
+ const rawSegments = normalized
36
+ .split(/(\*\*[^*]+\*\*|__[^_]+__|\*[^*]+\*|_[^_]+_|<u>[^<]+<\/u>)/)
37
+ .filter(Boolean);
38
+ return rawSegments.map((segment) => {
39
+ const baseStyles = { bold: false, italic: false, underline: false };
40
+ if (/^\*\*(.+)\*\*$/.test(segment) || /^__(.+)__$/.test(segment)) {
41
+ const content = segment.slice(2, -2);
42
+ return {
43
+ text: restoreEscapes(content),
44
+ styles: { ...baseStyles, bold: true },
45
+ };
46
+ }
47
+ if (/^\*(.+)\*$/.test(segment) || /^_(.+)_$/.test(segment)) {
48
+ const content = segment.slice(1, -1);
49
+ return {
50
+ text: restoreEscapes(content),
51
+ styles: { ...baseStyles, italic: true },
52
+ };
53
+ }
54
+ if (/^<u>(.+)<\/u>$/.test(segment)) {
55
+ const content = segment.slice(3, -4);
56
+ return {
57
+ text: restoreEscapes(content),
58
+ styles: { ...baseStyles, underline: true },
59
+ };
60
+ }
61
+ return {
62
+ text: restoreEscapes(segment),
63
+ styles: { ...baseStyles },
64
+ };
65
+ });
66
+ }
67
+ function inlineToHtml(inline) {
68
+ return inline
69
+ .map(({ text, styles }) => {
70
+ let html = escapeHtml(text);
71
+ if (styles.bold) {
72
+ html = `<strong>${html}</strong>`;
73
+ }
74
+ if (styles.italic) {
75
+ html = `<em>${html}</em>`;
76
+ }
77
+ if (styles.underline) {
78
+ html = `<u>${html}</u>`;
79
+ }
80
+ return html;
81
+ })
82
+ .join("");
83
+ }
84
+ function restoreEscapes(text) {
85
+ return text.replace(/\uE000/g, "\\");
86
+ }
87
+ function htmlToMarkdown(html) {
88
+ if (typeof document === "undefined") {
89
+ return fallbackHtmlToMarkdown(html);
90
+ }
91
+ const temp = document.createElement("div");
92
+ temp.innerHTML = html;
93
+ const traverse = (node) => {
94
+ var _a, _b, _c;
95
+ if (node.nodeType === Node.TEXT_NODE) {
96
+ const text = (_a = node.textContent) !== null && _a !== void 0 ? _a : "";
97
+ return escapeMarkdownText(text);
98
+ }
99
+ if (node.nodeType !== Node.ELEMENT_NODE) {
100
+ return "";
101
+ }
102
+ const element = node;
103
+ const children = Array.from(element.childNodes)
104
+ .map(traverse)
105
+ .join("");
106
+ switch (element.tagName.toLowerCase()) {
107
+ case "strong":
108
+ case "b":
109
+ return children ? `**${children}**` : children;
110
+ case "em":
111
+ case "i":
112
+ return children ? `*${children}*` : children;
113
+ case "u":
114
+ return children ? `<u>${children}</u>` : children;
115
+ case "br":
116
+ return "\n";
117
+ case "div":
118
+ case "p":
119
+ return children + "\n";
120
+ case "img": {
121
+ const src = (_b = element.getAttribute("src")) !== null && _b !== void 0 ? _b : "";
122
+ const alt = (_c = element.getAttribute("alt")) !== null && _c !== void 0 ? _c : "";
123
+ return `![${alt}](${src})`;
124
+ }
125
+ default:
126
+ return children;
127
+ }
128
+ };
129
+ const markdown = Array.from(temp.childNodes).map(traverse).join("");
130
+ return markdown.replace(/\n{3,}/g, "\n\n").trim();
131
+ }
132
+ function fallbackHtmlToMarkdown(html) {
133
+ if (!html) {
134
+ return "";
135
+ }
136
+ let result = html;
137
+ result = result.replace(/<img[^>]*>/gi, (match) => {
138
+ var _a, _b, _c, _d;
139
+ const src = (_b = (_a = match.match(/src="([^"]*)"/i)) === null || _a === void 0 ? void 0 : _a[1]) !== null && _b !== void 0 ? _b : "";
140
+ const alt = (_d = (_c = match.match(/alt="([^"]*)"/i)) === null || _c === void 0 ? void 0 : _c[1]) !== null && _d !== void 0 ? _d : "";
141
+ return `![${alt}](${src})`;
142
+ });
143
+ result = result
144
+ .replace(/<br\s*\/?>/gi, "\n")
145
+ .replace(/<\/?(div|p)>/gi, "\n")
146
+ .replace(/<strong>(.*?)<\/strong>/gis, (_m, content) => `**${content}**`)
147
+ .replace(/<(em|i)>(.*?)<\/(em|i)>/gis, (_m, _tag, content) => `*${content}*`)
148
+ .replace(/<span[^>]*>/gi, "")
149
+ .replace(/<\/span>/gi, "")
150
+ .replace(/<u>(.*?)<\/u>/gis, (_m, content) => `<u>${content}</u>`);
151
+ result = result.replace(/<\/?[^>]+>/g, "");
152
+ return result
153
+ .split("\n")
154
+ .map((line) => line.trimEnd())
155
+ .join("\n")
156
+ .replace(/\n{3,}/g, "\n\n")
157
+ .trim();
158
+ }
159
+ const MARKDOWN_ESCAPE_REGEX = /([*_\\])/g;
160
+ function escapeMarkdownText(text) {
161
+ return text.replace(MARKDOWN_ESCAPE_REGEX, "\\$1");
162
+ }
163
+ function StepField({ label, value, placeholder, onChange, autoFocus, multiline = false }) {
164
+ const editorRef = useRef(null);
165
+ const [isFocused, setIsFocused] = useState(false);
166
+ const autoFocusRef = useRef(false);
167
+ useEffect(() => {
168
+ const element = editorRef.current;
169
+ if (!element || isFocused) {
170
+ return;
171
+ }
172
+ if (value.trim().length === 0) {
173
+ element.innerHTML = "";
174
+ }
175
+ else {
176
+ element.innerHTML = markdownToHtml(value);
177
+ }
178
+ }, [value, isFocused]);
179
+ const syncValue = useCallback(() => {
180
+ const element = editorRef.current;
181
+ if (!element) {
182
+ return;
183
+ }
184
+ const markdown = htmlToMarkdown(element.innerHTML);
185
+ if (markdown !== value) {
186
+ onChange(markdown);
187
+ }
188
+ if (!markdown && element.innerHTML !== "") {
189
+ element.innerHTML = "";
190
+ }
191
+ }, [onChange, value]);
192
+ useEffect(() => {
193
+ if (autoFocus && !autoFocusRef.current && editorRef.current) {
194
+ editorRef.current.focus();
195
+ setIsFocused(true);
196
+ autoFocusRef.current = true;
197
+ }
198
+ }, [autoFocus]);
199
+ const applyFormat = useCallback((command) => {
200
+ document.execCommand(command);
201
+ syncValue();
202
+ }, [syncValue]);
203
+ const handlePaste = useCallback((event) => {
204
+ var _a, _b;
205
+ event.preventDefault();
206
+ const text = (_b = (_a = event.clipboardData) === null || _a === void 0 ? void 0 : _a.getData("text/plain")) !== null && _b !== void 0 ? _b : "";
207
+ document.execCommand("insertText", false, text);
208
+ syncValue();
209
+ }, [syncValue]);
210
+ return (_jsxs("div", { className: "bn-step-field", children: [_jsxs("div", { className: "bn-step-field__top", children: [_jsx("span", { className: "bn-step-field__label", children: label }), _jsxs("div", { className: "bn-step-toolbar", "aria-label": `${label} formatting`, children: [_jsx("button", { type: "button", className: "bn-step-toolbar__button", onMouseDown: (event) => {
211
+ var _a;
212
+ event.preventDefault();
213
+ (_a = editorRef.current) === null || _a === void 0 ? void 0 : _a.focus();
214
+ applyFormat("bold");
215
+ }, "aria-label": "Bold", children: "B" }), _jsx("button", { type: "button", className: "bn-step-toolbar__button", onMouseDown: (event) => {
216
+ var _a;
217
+ event.preventDefault();
218
+ (_a = editorRef.current) === null || _a === void 0 ? void 0 : _a.focus();
219
+ applyFormat("italic");
220
+ }, "aria-label": "Italic", children: "I" }), _jsx("button", { type: "button", className: "bn-step-toolbar__button", onMouseDown: (event) => {
221
+ var _a;
222
+ event.preventDefault();
223
+ (_a = editorRef.current) === null || _a === void 0 ? void 0 : _a.focus();
224
+ applyFormat("underline");
225
+ }, "aria-label": "Underline", children: "U" })] })] }), _jsx("div", { ref: editorRef, className: "bn-step-editor", contentEditable: true, suppressContentEditableWarning: true, "data-placeholder": placeholder, "data-multiline": multiline ? "true" : "false", onFocus: () => setIsFocused(true), onBlur: () => {
226
+ setIsFocused(false);
227
+ syncValue();
228
+ }, onInput: syncValue, onPaste: handlePaste, onKeyDown: (event) => {
229
+ if (event.key === "Enter") {
230
+ event.preventDefault();
231
+ if (multiline && event.shiftKey) {
232
+ document.execCommand("insertLineBreak");
233
+ document.execCommand("insertLineBreak");
234
+ }
235
+ else {
236
+ document.execCommand("insertLineBreak");
237
+ }
238
+ syncValue();
239
+ }
240
+ } })] }));
241
+ }
242
+ const statusOptions = ["draft", "ready", "blocked"];
243
+ const statusLabels = {
244
+ draft: "Draft",
245
+ ready: "Ready",
246
+ blocked: "Blocked",
247
+ };
248
+ const statusClassNames = {
249
+ draft: "bn-testcase--draft",
250
+ ready: "bn-testcase--ready",
251
+ blocked: "bn-testcase--blocked",
252
+ };
253
+ const testStepBlock = createReactBlockSpec({
254
+ type: "testStep",
255
+ content: "none",
256
+ propSchema: {
257
+ stepTitle: {
258
+ default: "",
259
+ },
260
+ stepData: {
261
+ default: "",
262
+ },
263
+ expectedResult: {
264
+ default: "",
265
+ },
266
+ },
267
+ }, {
268
+ render: ({ block, editor }) => {
269
+ const stepTitle = block.props.stepTitle || "";
270
+ const stepData = block.props.stepData || "";
271
+ const expectedResult = block.props.expectedResult || "";
272
+ const showExpectedField = stepTitle.trim().length > 0 || stepData.trim().length > 0 || expectedResult.trim().length > 0;
273
+ const [isDataVisible, setIsDataVisible] = useState(() => stepData.trim().length > 0);
274
+ const [shouldFocusDataField, setShouldFocusDataField] = useState(false);
275
+ useEffect(() => {
276
+ if (stepData.trim().length > 0 && !isDataVisible) {
277
+ setIsDataVisible(true);
278
+ }
279
+ }, [isDataVisible, stepData]);
280
+ useEffect(() => {
281
+ if (shouldFocusDataField && isDataVisible) {
282
+ const timer = setTimeout(() => setShouldFocusDataField(false), 0);
283
+ return () => clearTimeout(timer);
284
+ }
285
+ return undefined;
286
+ }, [isDataVisible, shouldFocusDataField]);
287
+ const handleStepTitleChange = useCallback((next) => {
288
+ if (next === stepTitle) {
289
+ return;
290
+ }
291
+ editor.updateBlock(block.id, {
292
+ props: {
293
+ stepTitle: next,
294
+ },
295
+ });
296
+ }, [editor, block.id, stepTitle]);
297
+ const handleStepDataChange = useCallback((next) => {
298
+ if (next === stepData) {
299
+ return;
300
+ }
301
+ editor.updateBlock(block.id, {
302
+ props: {
303
+ stepData: next,
304
+ },
305
+ });
306
+ }, [editor, block.id, stepData]);
307
+ const handleShowDataField = useCallback(() => {
308
+ setIsDataVisible(true);
309
+ setShouldFocusDataField(true);
310
+ }, []);
311
+ const handleExpectedChange = useCallback((next) => {
312
+ if (next === expectedResult) {
313
+ return;
314
+ }
315
+ editor.updateBlock(block.id, {
316
+ props: {
317
+ expectedResult: next,
318
+ },
319
+ });
320
+ }, [editor, block.id, expectedResult]);
321
+ return (_jsxs("div", { className: "bn-teststep", children: [_jsx(StepField, { label: "Step Title", value: stepTitle, placeholder: "Describe the action to perform", onChange: handleStepTitleChange, autoFocus: stepTitle.length === 0 }), !isDataVisible && (_jsx("button", { type: "button", className: "bn-teststep__toggle", onClick: handleShowDataField, "aria-expanded": "false", children: "[+ Data]" })), isDataVisible && (_jsx(StepField, { label: "Step Data", value: stepData, placeholder: "Provide additional data about the step", onChange: handleStepDataChange, autoFocus: shouldFocusDataField, multiline: true })), showExpectedField && (_jsx(StepField, { label: "Expected Result", value: expectedResult, placeholder: "What should happen?", onChange: handleExpectedChange, multiline: true }))] }));
322
+ },
323
+ });
324
+ const testCaseBlock = createReactBlockSpec({
325
+ type: "testCase",
326
+ content: "inline",
327
+ propSchema: {
328
+ textAlignment: defaultProps.textAlignment,
329
+ textColor: defaultProps.textColor,
330
+ backgroundColor: defaultProps.backgroundColor,
331
+ status: {
332
+ default: "draft",
333
+ values: Array.from(statusOptions),
334
+ },
335
+ reference: {
336
+ default: "",
337
+ },
338
+ },
339
+ }, {
340
+ render: ({ block, contentRef, editor }) => {
341
+ const status = block.props.status;
342
+ const handleStatusChange = (event) => {
343
+ const nextStatus = event.target.value;
344
+ editor.updateBlock(block.id, {
345
+ props: {
346
+ status: nextStatus,
347
+ },
348
+ });
349
+ };
350
+ const handleReferenceChange = (event) => {
351
+ editor.updateBlock(block.id, {
352
+ props: {
353
+ reference: event.target.value,
354
+ },
355
+ });
356
+ };
357
+ const style = {
358
+ textAlign: block.props.textAlignment,
359
+ color: block.props.textColor === "default"
360
+ ? undefined
361
+ : block.props.textColor,
362
+ backgroundColor: block.props.backgroundColor === "default"
363
+ ? undefined
364
+ : block.props.backgroundColor,
365
+ };
366
+ return (_jsxs("div", { className: "bn-testcase " + statusClassNames[status], "data-reference": block.props.reference || undefined, style: style, children: [_jsxs("div", { className: "bn-testcase__header", children: [_jsxs("div", { className: "bn-testcase__meta", children: [_jsx("span", { className: "bn-testcase__label", children: "Test Case" }), _jsx("input", { className: "bn-testcase__reference", placeholder: "Reference ID", value: block.props.reference, onChange: handleReferenceChange })] }), _jsxs("label", { className: "bn-testcase__status", children: [_jsx("span", { children: "Status:" }), _jsx("select", { value: status, onChange: handleStatusChange, children: statusOptions.map((option) => (_jsx("option", { value: option, children: statusLabels[option] }, option))) })] })] }), _jsx("div", { className: "bn-testcase__body", ref: contentRef })] }));
367
+ },
368
+ });
369
+ export const customSchema = BlockNoteSchema.create({
370
+ blockSpecs: {
371
+ ...defaultBlockSpecs,
372
+ testCase: testCaseBlock,
373
+ testStep: testStepBlock,
374
+ },
375
+ });
376
+ export const __markdownTestUtils = {
377
+ markdownToHtml,
378
+ htmlToMarkdown,
379
+ };
@@ -0,0 +1,3 @@
1
+ export { customSchema, type CustomSchema, type CustomBlock, type CustomEditor, } from "./editor/customSchema";
2
+ export { blocksToMarkdown, markdownToBlocks, type CustomEditorBlock, type CustomPartialBlock, } from "./editor/customMarkdownConverter";
3
+ export declare const testomatioEditorClassName = "markdown testomatio-editor";
@@ -0,0 +1,3 @@
1
+ export { customSchema, } from "./editor/customSchema";
2
+ export { blocksToMarkdown, markdownToBlocks, } from "./editor/customMarkdownConverter";
3
+ export const testomatioEditorClassName = "markdown testomatio-editor";
@@ -0,0 +1,238 @@
1
+ .testomatio-editor {
2
+ font-family: "Inter", system-ui, -apple-system, sans-serif;
3
+ }
4
+
5
+ .bn-testcase {
6
+ --status-color: #94a3b8;
7
+ position: relative;
8
+ border-radius: 1rem;
9
+ border-left: 6px solid var(--status-color);
10
+ padding: 1rem 1.25rem;
11
+ background: rgba(148, 163, 184, 0.12);
12
+ display: flex;
13
+ flex-direction: column;
14
+ gap: 0.75rem;
15
+ }
16
+
17
+ .bn-testcase::after {
18
+ content: "";
19
+ position: absolute;
20
+ inset: 0;
21
+ border-radius: inherit;
22
+ pointer-events: none;
23
+ box-shadow: 0 0 0 1px rgba(15, 23, 42, 0.05);
24
+ }
25
+
26
+ .bn-testcase__header {
27
+ display: flex;
28
+ flex-wrap: wrap;
29
+ justify-content: space-between;
30
+ align-items: center;
31
+ gap: 0.75rem;
32
+ }
33
+
34
+ .bn-testcase__meta {
35
+ display: flex;
36
+ align-items: center;
37
+ gap: 0.5rem;
38
+ }
39
+
40
+ .bn-testcase__label {
41
+ font-size: 0.85rem;
42
+ font-weight: 700;
43
+ text-transform: uppercase;
44
+ letter-spacing: 0.08em;
45
+ color: #0f172a;
46
+ }
47
+
48
+ .bn-testcase__reference {
49
+ padding: 0.35rem 0.6rem;
50
+ border-radius: 0.5rem;
51
+ border: 1px solid rgba(15, 23, 42, 0.1);
52
+ background: rgba(255, 255, 255, 0.85);
53
+ font-size: 0.85rem;
54
+ min-width: 8rem;
55
+ }
56
+
57
+ .bn-testcase__reference:focus-visible {
58
+ outline: 2px solid rgba(37, 99, 235, 0.4);
59
+ outline-offset: 2px;
60
+ }
61
+
62
+ .bn-testcase__status {
63
+ display: flex;
64
+ align-items: center;
65
+ gap: 0.35rem;
66
+ font-size: 0.85rem;
67
+ color: #0f172a;
68
+ font-weight: 600;
69
+ }
70
+
71
+ .bn-testcase__status select {
72
+ border-radius: 0.5rem;
73
+ border: 1px solid rgba(15, 23, 42, 0.12);
74
+ padding: 0.35rem 0.6rem;
75
+ background: rgba(255, 255, 255, 0.95);
76
+ font-size: 0.85rem;
77
+ font-weight: 600;
78
+ color: inherit;
79
+ }
80
+
81
+ .bn-testcase__body {
82
+ min-height: 2.5rem;
83
+ }
84
+
85
+ .bn-testcase--ready {
86
+ --status-color: #22c55e;
87
+ background: rgba(34, 197, 94, 0.12);
88
+ }
89
+
90
+ .bn-testcase--blocked {
91
+ --status-color: #ef4444;
92
+ background: rgba(239, 68, 68, 0.12);
93
+ }
94
+
95
+ .bn-teststep {
96
+ border-radius: 0.85rem;
97
+ border-left: 5px solid rgba(59, 130, 246, 0.65);
98
+ background: rgba(59, 130, 246, 0.12);
99
+ padding: 0.85rem 1.1rem 1rem;
100
+ display: grid;
101
+ gap: 0.75rem;
102
+ position: relative;
103
+ width: 100%;
104
+ }
105
+
106
+ .bn-teststep::after {
107
+ content: "";
108
+ position: absolute;
109
+ inset: 0;
110
+ border-radius: inherit;
111
+ pointer-events: none;
112
+ box-shadow: 0 0 0 1px rgba(37, 99, 235, 0.08);
113
+ }
114
+
115
+ .bn-teststep__toggle {
116
+ align-self: flex-start;
117
+ padding: 0.35rem 0.6rem;
118
+ border-radius: 0.5rem;
119
+ border: 1px dashed rgba(37, 99, 235, 0.45);
120
+ background: rgba(59, 130, 246, 0.1);
121
+ color: #1d4ed8;
122
+ font-size: 0.75rem;
123
+ font-weight: 700;
124
+ letter-spacing: 0.06em;
125
+ text-transform: uppercase;
126
+ cursor: pointer;
127
+ transition:
128
+ background-color 120ms ease,
129
+ border-color 120ms ease,
130
+ box-shadow 120ms ease;
131
+ }
132
+
133
+ .bn-teststep__toggle:hover {
134
+ background: rgba(59, 130, 246, 0.18);
135
+ border-color: rgba(37, 99, 235, 0.55);
136
+ box-shadow: 0 0 0 1px rgba(37, 99, 235, 0.2);
137
+ }
138
+
139
+ .bn-step-field {
140
+ display: flex;
141
+ flex-direction: column;
142
+ gap: 0.35rem;
143
+ }
144
+
145
+ .bn-step-field__top {
146
+ display: flex;
147
+ justify-content: space-between;
148
+ align-items: center;
149
+ gap: 0.5rem;
150
+ }
151
+
152
+ .bn-step-field__label {
153
+ font-size: 0.8rem;
154
+ text-transform: uppercase;
155
+ letter-spacing: 0.08em;
156
+ font-weight: 700;
157
+ color: #1d4ed8;
158
+ }
159
+
160
+ .bn-step-toolbar {
161
+ display: flex;
162
+ gap: 0.35rem;
163
+ }
164
+
165
+ .bn-step-toolbar__button {
166
+ width: 2rem;
167
+ height: 2rem;
168
+ border-radius: 0.4rem;
169
+ border: 1px solid rgba(37, 99, 235, 0.35);
170
+ background: rgba(255, 255, 255, 0.85);
171
+ color: #1d4ed8;
172
+ font-weight: 700;
173
+ font-size: 0.9rem;
174
+ display: flex;
175
+ align-items: center;
176
+ justify-content: center;
177
+ cursor: pointer;
178
+ transition:
179
+ background-color 120ms ease,
180
+ border-color 120ms ease,
181
+ box-shadow 120ms ease;
182
+ }
183
+
184
+ .bn-step-toolbar__button:hover {
185
+ background: rgba(37, 99, 235, 0.12);
186
+ border-color: rgba(37, 99, 235, 0.55);
187
+ }
188
+
189
+ .bn-step-editor {
190
+ border-radius: 0.6rem;
191
+ border: 1px solid rgba(37, 99, 235, 0.25);
192
+ padding: 0.5rem 0.75rem;
193
+ font-size: 0.95rem;
194
+ background: rgba(255, 255, 255, 0.92);
195
+ transition:
196
+ border-color 120ms ease,
197
+ box-shadow 120ms ease;
198
+ min-height: 2.5rem;
199
+ white-space: pre-wrap;
200
+ word-break: break-word;
201
+ }
202
+
203
+ .bn-step-editor[data-multiline="true"] {
204
+ min-height: 4rem;
205
+ }
206
+
207
+ .bn-step-editor:focus-visible {
208
+ outline: none;
209
+ border-color: rgba(37, 99, 235, 0.7);
210
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);
211
+ }
212
+
213
+ .bn-step-editor:empty::before {
214
+ content: attr(data-placeholder);
215
+ color: rgba(15, 23, 42, 0.45);
216
+ pointer-events: none;
217
+ }
218
+
219
+ .bn-inline-image {
220
+ display: block;
221
+ max-width: 100%;
222
+ border-radius: 0.65rem;
223
+ margin: 0.5rem 0;
224
+ pointer-events: none;
225
+ }
226
+
227
+ .bn-suggestion-icon {
228
+ display: inline-flex;
229
+ align-items: center;
230
+ justify-content: center;
231
+ width: 20px;
232
+ height: 20px;
233
+ font-size: 14px;
234
+ }
235
+
236
+ .bn-testcase--draft {
237
+ --status-color: #94a3b8;
238
+ }
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "testomatio-editor-blocks",
3
+ "version": "0.1.0",
4
+ "description": "Custom BlockNote schema, markdown conversion helpers, and UI for Testomatio-style test cases and steps.",
5
+ "type": "module",
6
+ "main": "./package/index.js",
7
+ "module": "./package/index.js",
8
+ "types": "./package/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./package/index.d.ts",
12
+ "import": "./package/index.js"
13
+ },
14
+ "./styles.css": "./package/styles.css",
15
+ "./package.json": "./package.json"
16
+ },
17
+ "files": [
18
+ "package",
19
+ "src",
20
+ "README.md"
21
+ ],
22
+ "scripts": {
23
+ "dev": "vite",
24
+ "build": "tsc -b && npm run build:package && vite build",
25
+ "build:package": "node scripts/build-package.mjs",
26
+ "lint": "eslint .",
27
+ "test": "vitest",
28
+ "test:run": "vitest run",
29
+ "preview": "vite preview"
30
+ },
31
+ "keywords": [
32
+ "blocknote",
33
+ "editor",
34
+ "markdown",
35
+ "testcases",
36
+ "test-automation"
37
+ ],
38
+ "peerDependencies": {
39
+ "@blocknote/core": "^0.31.3",
40
+ "@blocknote/react": "^0.31.3",
41
+ "react": "^18.0.0 || ^19.0.0",
42
+ "react-dom": "^18.0.0 || ^19.0.0"
43
+ },
44
+ "devDependencies": {
45
+ "@blocknote/core": "^0.31.3",
46
+ "@blocknote/mantine": "^0.31.3",
47
+ "@blocknote/react": "^0.31.3",
48
+ "@eslint/js": "^9.36.0",
49
+ "@types/node": "^24.6.0",
50
+ "@types/react": "^19.1.16",
51
+ "@types/react-dom": "^19.1.9",
52
+ "@vitejs/plugin-react": "^5.0.4",
53
+ "eslint": "^9.36.0",
54
+ "eslint-plugin-react-hooks": "^5.2.0",
55
+ "eslint-plugin-react-refresh": "^0.4.22",
56
+ "globals": "^16.4.0",
57
+ "react": "^19.1.1",
58
+ "react-dom": "^19.1.1",
59
+ "typescript": "~5.9.3",
60
+ "typescript-eslint": "^8.45.0",
61
+ "vite": "^7.1.7",
62
+ "vitest": "^3.2.4"
63
+ }
64
+ }