testomatio-editor-blocks 0.4.65 → 0.4.66

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,37 @@
1
+ export type MetaField = {
2
+ key: string;
3
+ value: string;
4
+ };
5
+ export declare function serializeMetaFields(fields: MetaField[]): string;
6
+ export declare const testMetaBlock: {
7
+ config: {
8
+ readonly type: "testMeta";
9
+ readonly content: "none";
10
+ readonly propSchema: {
11
+ readonly metaKind: {
12
+ readonly default: "test";
13
+ };
14
+ readonly metaFields: {
15
+ readonly default: "[]";
16
+ };
17
+ readonly metaInline: {
18
+ readonly default: false;
19
+ };
20
+ };
21
+ };
22
+ implementation: import("@blocknote/core").TiptapBlockImplementation<{
23
+ readonly type: "testMeta";
24
+ readonly content: "none";
25
+ readonly propSchema: {
26
+ readonly metaKind: {
27
+ readonly default: "test";
28
+ };
29
+ readonly metaFields: {
30
+ readonly default: "[]";
31
+ };
32
+ readonly metaInline: {
33
+ readonly default: false;
34
+ };
35
+ };
36
+ }, any, import("@blocknote/core").InlineContentSchema, import("@blocknote/core").StyleSchema>;
37
+ };
@@ -0,0 +1,111 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { createReactBlockSpec } from "@blocknote/react";
3
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
4
+ import { getMetaFieldSuggestions } from "../testMetaFields";
5
+ const ID_KEYS = new Set(["id"]);
6
+ function parseMetaFields(raw) {
7
+ if (typeof raw !== "string" || raw.trim().length === 0) {
8
+ return [];
9
+ }
10
+ try {
11
+ const parsed = JSON.parse(raw);
12
+ if (!Array.isArray(parsed)) {
13
+ return [];
14
+ }
15
+ return parsed
16
+ .filter((item) => item && typeof item === "object")
17
+ .map((item) => ({
18
+ key: typeof item.key === "string" ? item.key : "",
19
+ value: typeof item.value === "string" ? item.value : "",
20
+ }));
21
+ }
22
+ catch {
23
+ return [];
24
+ }
25
+ }
26
+ export function serializeMetaFields(fields) {
27
+ return JSON.stringify(fields);
28
+ }
29
+ function AddFieldMenu({ kind, usedKeys, onPick }) {
30
+ const [isOpen, setIsOpen] = useState(false);
31
+ const containerRef = useRef(null);
32
+ useEffect(() => {
33
+ if (!isOpen)
34
+ return;
35
+ const handleMouseDown = (event) => {
36
+ if (containerRef.current && !containerRef.current.contains(event.target)) {
37
+ setIsOpen(false);
38
+ }
39
+ };
40
+ document.addEventListener("mousedown", handleMouseDown);
41
+ return () => document.removeEventListener("mousedown", handleMouseDown);
42
+ }, [isOpen]);
43
+ const available = useMemo(() => {
44
+ const used = new Set(usedKeys.map((k) => k.trim().toLowerCase()));
45
+ return getMetaFieldSuggestions(kind).filter((s) => !used.has(s.key.trim().toLowerCase()));
46
+ }, [kind, usedKeys]);
47
+ const pick = useCallback((key) => {
48
+ onPick(key);
49
+ setIsOpen(false);
50
+ }, [onPick]);
51
+ return (_jsxs("div", { className: "bn-testmeta__add-wrap", ref: containerRef, children: [_jsx("button", { type: "button", className: "bn-testmeta__add", "aria-label": "Add field", title: "Add field", onClick: () => setIsOpen((prev) => !prev), children: "+" }), isOpen && (_jsxs("div", { className: "bn-testmeta__menu", role: "listbox", children: [available.map((suggestion) => {
52
+ var _a;
53
+ return (_jsx("button", { type: "button", role: "option", className: "bn-testmeta__menu-item", onMouseDown: (event) => {
54
+ event.preventDefault();
55
+ pick(suggestion.key);
56
+ }, children: (_a = suggestion.label) !== null && _a !== void 0 ? _a : suggestion.key }, suggestion.key));
57
+ }), _jsx("button", { type: "button", className: "bn-testmeta__menu-item bn-testmeta__menu-item--custom", onMouseDown: (event) => {
58
+ event.preventDefault();
59
+ pick("");
60
+ }, children: "Custom field\u2026" })] }))] }));
61
+ }
62
+ export const testMetaBlock = createReactBlockSpec({
63
+ type: "testMeta",
64
+ content: "none",
65
+ propSchema: {
66
+ // "test" | "suite" — which keyword the comment opened with.
67
+ metaKind: {
68
+ default: "test",
69
+ },
70
+ // JSON-encoded MetaField[] so insertion order is preserved.
71
+ metaFields: {
72
+ default: "[]",
73
+ },
74
+ // true when the source comment was a one-liner (`<!-- test id: @T.. -->`).
75
+ metaInline: {
76
+ default: false,
77
+ },
78
+ },
79
+ }, {
80
+ render: ({ block, editor }) => {
81
+ const kind = block.props.metaKind === "suite" ? "suite" : "test";
82
+ const fields = useMemo(() => parseMetaFields(block.props.metaFields), [block.props.metaFields]);
83
+ const commitFields = useCallback((next) => {
84
+ editor.updateBlock(block.id, {
85
+ props: { metaFields: serializeMetaFields(next) },
86
+ });
87
+ }, [block.id, editor]);
88
+ const handleValueChange = useCallback((index, value) => {
89
+ const next = fields.map((field, i) => i === index ? { ...field, value } : field);
90
+ commitFields(next);
91
+ }, [fields, commitFields]);
92
+ const handleKeyChange = useCallback((index, key) => {
93
+ const next = fields.map((field, i) => i === index ? { ...field, key } : field);
94
+ commitFields(next);
95
+ }, [fields, commitFields]);
96
+ const handleRemove = useCallback((index) => {
97
+ commitFields(fields.filter((_, i) => i !== index));
98
+ }, [fields, commitFields]);
99
+ const handleAddField = useCallback((key) => {
100
+ commitFields([...fields, { key, value: "" }]);
101
+ }, [fields, commitFields]);
102
+ const usedKeys = useMemo(() => fields.map((f) => f.key), [fields]);
103
+ // The read-only `id` is shown inline on the header line next to the kind
104
+ // label; every other field renders as an editable row below.
105
+ const idField = fields.find((f) => ID_KEYS.has(f.key.trim().toLowerCase()));
106
+ const editableFields = fields
107
+ .map((field, index) => ({ field, index }))
108
+ .filter(({ field }) => !ID_KEYS.has(field.key.trim().toLowerCase()));
109
+ return (_jsxs("div", { className: "bn-testmeta", "data-block-id": block.id, "data-kind": kind, contentEditable: false, suppressContentEditableWarning: true, draggable: false, children: [_jsxs("div", { className: "bn-testmeta__header", children: [_jsx("span", { className: "bn-testmeta__label", children: kind.toUpperCase() }), (idField === null || idField === void 0 ? void 0 : idField.value) && _jsx("span", { className: "bn-testmeta__id", children: idField.value }), _jsx(AddFieldMenu, { kind: kind, usedKeys: usedKeys, onPick: handleAddField })] }), editableFields.length > 0 && (_jsx("div", { className: "bn-testmeta__rows", children: editableFields.map(({ field, index }) => (_jsxs("div", { className: "bn-testmeta__row", children: [_jsx("input", { className: "bn-testmeta__key bn-testmeta__key--input", type: "text", value: field.key, placeholder: "key", spellCheck: false, onChange: (e) => handleKeyChange(index, e.target.value) }), _jsx("input", { className: "bn-testmeta__value", type: "text", value: field.value, placeholder: "value", spellCheck: false, onChange: (e) => handleValueChange(index, e.target.value) }), _jsx("button", { type: "button", className: "bn-testmeta__remove", "aria-label": "Remove field", title: "Remove field", onClick: () => handleRemove(index), children: "\u00D7" })] }, index))) }))] }));
110
+ },
111
+ });
@@ -224,7 +224,7 @@ function serializeChildren(block, ctx) {
224
224
  return serializeBlocks(block.children, childCtx);
225
225
  }
226
226
  function serializeBlock(block, ctx, orderedIndex, stepIndex) {
227
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
227
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p;
228
228
  const lines = [];
229
229
  const indent = ctx.listDepth > 0 ? " ".repeat(ctx.listDepth) : "";
230
230
  switch (block.type) {
@@ -316,19 +316,48 @@ function serializeBlock(block, ctx, orderedIndex, stepIndex) {
316
316
  }
317
317
  return lines;
318
318
  }
319
+ case "testMeta": {
320
+ const kind = block.props.metaKind === "suite" ? "suite" : "test";
321
+ const inline = Boolean(block.props.metaInline);
322
+ let fields = [];
323
+ try {
324
+ const parsed = JSON.parse(((_e = block.props.metaFields) !== null && _e !== void 0 ? _e : "[]"));
325
+ if (Array.isArray(parsed)) {
326
+ fields = parsed
327
+ .filter((f) => f && typeof f === "object" && typeof f.key === "string")
328
+ .map((f) => ({ key: f.key.trim(), value: typeof f.value === "string" ? f.value.trim() : "" }))
329
+ // Skip incomplete fields: both a key and a value are required.
330
+ .filter((f) => f.key.length > 0 && f.value.length > 0);
331
+ }
332
+ }
333
+ catch {
334
+ fields = [];
335
+ }
336
+ // Preserve the one-liner form only when it still fits on a single line
337
+ // (a one-liner holds at most one `key: value` pair).
338
+ if (inline && fields.length <= 1) {
339
+ const field = fields[0];
340
+ lines.push(field ? `<!-- ${kind} ${field.key}: ${field.value} -->` : `<!-- ${kind} -->`);
341
+ return lines;
342
+ }
343
+ lines.push(`<!-- ${kind}`);
344
+ fields.forEach((field) => lines.push(`${field.key}: ${field.value}`));
345
+ lines.push("-->");
346
+ return lines;
347
+ }
319
348
  case "testStep":
320
349
  case "snippet": {
321
350
  const isSnippet = block.type === "snippet";
322
- const snippetId = isSnippet ? ((_e = block.props.snippetId) !== null && _e !== void 0 ? _e : "").trim() : "";
351
+ const snippetId = isSnippet ? ((_f = block.props.snippetId) !== null && _f !== void 0 ? _f : "").trim() : "";
323
352
  const stepTitle = isSnippet
324
- ? ((_f = block.props.snippetTitle) !== null && _f !== void 0 ? _f : "").trim()
325
- : ((_g = block.props.stepTitle) !== null && _g !== void 0 ? _g : "").trim();
353
+ ? ((_g = block.props.snippetTitle) !== null && _g !== void 0 ? _g : "").trim()
354
+ : ((_h = block.props.stepTitle) !== null && _h !== void 0 ? _h : "").trim();
326
355
  const stepData = isSnippet
327
- ? ((_h = block.props.snippetData) !== null && _h !== void 0 ? _h : "").trim()
328
- : ((_j = block.props.stepData) !== null && _j !== void 0 ? _j : "").trim();
356
+ ? ((_j = block.props.snippetData) !== null && _j !== void 0 ? _j : "").trim()
357
+ : ((_k = block.props.stepData) !== null && _k !== void 0 ? _k : "").trim();
329
358
  const expectedResult = isSnippet
330
- ? ((_k = block.props.snippetExpectedResult) !== null && _k !== void 0 ? _k : "").trim()
331
- : ((_l = block.props.expectedResult) !== null && _l !== void 0 ? _l : "").trim();
359
+ ? ((_l = block.props.snippetExpectedResult) !== null && _l !== void 0 ? _l : "").trim()
360
+ : ((_m = block.props.expectedResult) !== null && _m !== void 0 ? _m : "").trim();
332
361
  if (isSnippet) {
333
362
  if (snippetId) {
334
363
  lines.push(`<!-- begin snippet #${snippetId} -->`);
@@ -352,7 +381,7 @@ function serializeBlock(block, ctx, orderedIndex, stepIndex) {
352
381
  const normalizedExpectedForCheck = stripExpectedPrefix(expectedResult).trim();
353
382
  const hasContent = stepData.length > 0 || normalizedExpectedForCheck.length > 0;
354
383
  if (normalizedTitle.length > 0 || hasContent) {
355
- const listStyle = (_m = block.props.listStyle) !== null && _m !== void 0 ? _m : "bullet";
384
+ const listStyle = (_o = block.props.listStyle) !== null && _o !== void 0 ? _o : "bullet";
356
385
  const prefix = listStyle === "ordered" ? `${(stepIndex !== null && stepIndex !== void 0 ? stepIndex : 0) + 1}.` : "*";
357
386
  lines.push(normalizedTitle.length > 0 ? `${prefix} ${escapeStepContent(normalizedTitle)}` : `${prefix} `);
358
387
  }
@@ -406,7 +435,7 @@ function serializeBlock(block, ctx, orderedIndex, stepIndex) {
406
435
  return lines;
407
436
  }
408
437
  const headerRowCount = rows.length
409
- ? Math.min(rows.length, Math.max((_o = tableContent.headerRows) !== null && _o !== void 0 ? _o : 1, 1))
438
+ ? Math.min(rows.length, Math.max((_p = tableContent.headerRows) !== null && _p !== void 0 ? _p : 1, 1))
410
439
  : 0;
411
440
  const columnAlignments = new Array(columnCount).fill("left");
412
441
  const getCellAlignment = (cell) => {
@@ -1145,6 +1174,86 @@ function parseParagraph(lines, index) {
1145
1174
  nextIndex: index + 1,
1146
1175
  };
1147
1176
  }
1177
+ const META_COMMENT_OPEN_REGEX = /^<!--\s*(test|suite)(?=\s|-->|$)/i;
1178
+ function metaFieldsFromBody(bodyLines) {
1179
+ const fields = [];
1180
+ for (const raw of bodyLines) {
1181
+ const line = raw.trim();
1182
+ if (!line)
1183
+ continue;
1184
+ const colon = line.indexOf(":");
1185
+ // "Each line is `key: value`; lines without `:` are ignored."
1186
+ if (colon === -1)
1187
+ continue;
1188
+ const key = line.slice(0, colon).trim();
1189
+ const value = line.slice(colon + 1).trim();
1190
+ if (!key)
1191
+ continue;
1192
+ fields.push({ key, value });
1193
+ }
1194
+ return fields;
1195
+ }
1196
+ function parseMetaComment(lines, index) {
1197
+ const first = lines[index];
1198
+ const openMatch = first.match(META_COMMENT_OPEN_REGEX);
1199
+ if (!openMatch) {
1200
+ return null;
1201
+ }
1202
+ const kind = openMatch[1].toLowerCase();
1203
+ let bodyLines = [];
1204
+ let inline = false;
1205
+ let nextIndex;
1206
+ // One-liner: opening and closing markers on the same line.
1207
+ const oneLine = first.match(/^<!--\s*(?:test|suite)\b\s*([\s\S]*?)\s*-->\s*$/i);
1208
+ if (oneLine) {
1209
+ inline = true;
1210
+ if (oneLine[1].trim()) {
1211
+ bodyLines = [oneLine[1].trim()];
1212
+ }
1213
+ nextIndex = index + 1;
1214
+ }
1215
+ else {
1216
+ // Block form: keyword line, fields on their own lines, closing `-->`.
1217
+ const afterKeyword = first.replace(/^<!--\s*(?:test|suite)\b/i, "").trim();
1218
+ if (afterKeyword) {
1219
+ bodyLines.push(afterKeyword);
1220
+ }
1221
+ let next = index + 1;
1222
+ let closed = false;
1223
+ while (next < lines.length) {
1224
+ const current = lines[next];
1225
+ if (/-->\s*$/.test(current)) {
1226
+ const beforeClose = current.replace(/-->\s*$/, "").trim();
1227
+ if (beforeClose) {
1228
+ bodyLines.push(beforeClose);
1229
+ }
1230
+ next += 1;
1231
+ closed = true;
1232
+ break;
1233
+ }
1234
+ bodyLines.push(current);
1235
+ next += 1;
1236
+ }
1237
+ if (!closed) {
1238
+ // Unterminated comment — let normal parsing handle these lines.
1239
+ return null;
1240
+ }
1241
+ nextIndex = next;
1242
+ }
1243
+ const fields = metaFieldsFromBody(bodyLines);
1244
+ return {
1245
+ block: {
1246
+ type: "testMeta",
1247
+ props: {
1248
+ metaKind: kind,
1249
+ metaFields: JSON.stringify(fields),
1250
+ metaInline: inline,
1251
+ },
1252
+ children: [],
1253
+ },
1254
+ nextIndex,
1255
+ };
1256
+ }
1148
1257
  function parseSnippetWrapper(lines, index) {
1149
1258
  const trimmed = lines[index].trim();
1150
1259
  const startMatch = trimmed.match(/^<!--\s*begin snippet\s*#?\s*([^\s>]+)\s*-->/i);
@@ -1257,6 +1366,14 @@ export function markdownToBlocks(markdown, _options) {
1257
1366
  index += 1;
1258
1367
  continue;
1259
1368
  }
1369
+ // Test/suite metadata comments can appear anywhere (typically at the top of
1370
+ // a document or right after a heading), so this runs ungated.
1371
+ const metaComment = parseMetaComment(lines, index);
1372
+ if (metaComment) {
1373
+ blocks.push(metaComment.block);
1374
+ index = metaComment.nextIndex;
1375
+ continue;
1376
+ }
1260
1377
  const snippetWrapper = stepsHeadingLevel !== null
1261
1378
  ? parseSnippetWrapper(lines, index)
1262
1379
  : null;
@@ -117,6 +117,38 @@ export declare const customSchema: BlockNoteSchema<import("@blocknote/core").Blo
117
117
  };
118
118
  }, any, import("@blocknote/core").InlineContentSchema, import("@blocknote/core").StyleSchema>;
119
119
  };
120
+ testMeta: {
121
+ config: {
122
+ readonly type: "testMeta";
123
+ readonly content: "none";
124
+ readonly propSchema: {
125
+ readonly metaKind: {
126
+ readonly default: "test";
127
+ };
128
+ readonly metaFields: {
129
+ readonly default: "[]";
130
+ };
131
+ readonly metaInline: {
132
+ readonly default: false;
133
+ };
134
+ };
135
+ };
136
+ implementation: import("@blocknote/core").TiptapBlockImplementation<{
137
+ readonly type: "testMeta";
138
+ readonly content: "none";
139
+ readonly propSchema: {
140
+ readonly metaKind: {
141
+ readonly default: "test";
142
+ };
143
+ readonly metaFields: {
144
+ readonly default: "[]";
145
+ };
146
+ readonly metaInline: {
147
+ readonly default: false;
148
+ };
149
+ };
150
+ }, any, import("@blocknote/core").InlineContentSchema, import("@blocknote/core").StyleSchema>;
151
+ };
120
152
  paragraph: {
121
153
  config: {
122
154
  type: "paragraph";
@@ -2,6 +2,7 @@ import { defaultBlockSpecs } from "@blocknote/core";
2
2
  import { BlockNoteSchema } from "@blocknote/core";
3
3
  import { stepBlock } from "./blocks/step";
4
4
  import { snippetBlock } from "./blocks/snippet";
5
+ import { testMetaBlock } from "./blocks/testMeta";
5
6
  import { fileBlock } from "./blocks/fileBlock";
6
7
  import { htmlToMarkdown, markdownToHtml } from "./blocks/markdown";
7
8
  export const customSchema = BlockNoteSchema.create({
@@ -10,6 +11,7 @@ export const customSchema = BlockNoteSchema.create({
10
11
  file: fileBlock,
11
12
  testStep: stepBlock,
12
13
  snippet: snippetBlock,
14
+ testMeta: testMetaBlock,
13
15
  },
14
16
  });
15
17
  export const __markdownTestUtils = {
@@ -0,0 +1,17 @@
1
+ export type MetaFieldSuggestion = {
2
+ /** The field key that gets inserted, e.g. "priority". */
3
+ key: string;
4
+ /** Optional display label; defaults to `key`. */
5
+ label?: string;
6
+ };
7
+ /**
8
+ * Either a flat list (applied to both test and suite blocks) or per-kind lists.
9
+ * Configure from the host app via `setMetaFieldSuggestions` so embedders can
10
+ * plug in their own set of suggested metadata fields.
11
+ */
12
+ export type MetaFieldSuggestionsConfig = MetaFieldSuggestion[] | {
13
+ test?: MetaFieldSuggestion[];
14
+ suite?: MetaFieldSuggestion[];
15
+ };
16
+ export declare function setMetaFieldSuggestions(config: MetaFieldSuggestionsConfig | null): void;
17
+ export declare function getMetaFieldSuggestions(kind: "test" | "suite"): MetaFieldSuggestion[];
@@ -0,0 +1,33 @@
1
+ // Defaults follow the classical Testomatio markdown format. `id` is intentionally
2
+ // omitted: it is a read-only, system-assigned field, not something users add.
3
+ const DEFAULT_TEST_FIELDS = [
4
+ { key: "priority" },
5
+ { key: "type" },
6
+ { key: "tags" },
7
+ { key: "labels" },
8
+ { key: "assignee" },
9
+ { key: "creator" },
10
+ { key: "shared" },
11
+ ];
12
+ const DEFAULT_SUITE_FIELDS = [
13
+ { key: "emoji" },
14
+ { key: "tags" },
15
+ { key: "labels" },
16
+ { key: "assignee" },
17
+ ];
18
+ let configured = null;
19
+ export function setMetaFieldSuggestions(config) {
20
+ configured = config;
21
+ }
22
+ export function getMetaFieldSuggestions(kind) {
23
+ if (configured) {
24
+ if (Array.isArray(configured)) {
25
+ return configured;
26
+ }
27
+ const list = kind === "suite" ? configured.suite : configured.test;
28
+ if (list) {
29
+ return list;
30
+ }
31
+ }
32
+ return kind === "suite" ? DEFAULT_SUITE_FIELDS : DEFAULT_TEST_FIELDS;
33
+ }
@@ -1,6 +1,8 @@
1
1
  export { customSchema, type CustomSchema, type CustomBlock, type CustomEditor, } from "./editor/customSchema";
2
2
  export { stepBlock, canInsertStepOrSnippet, isStepsHeading, addStepsBlock, addSnippetBlock } from "./editor/blocks/step";
3
3
  export { snippetBlock } from "./editor/blocks/snippet";
4
+ export { testMetaBlock } from "./editor/blocks/testMeta";
5
+ export { setMetaFieldSuggestions, getMetaFieldSuggestions, type MetaFieldSuggestion, type MetaFieldSuggestionsConfig, } from "./editor/testMetaFields";
4
6
  export { markdownToHtml, htmlToMarkdown } from "./editor/blocks/markdown";
5
7
  export { blocksToMarkdown, markdownToBlocks, type CustomEditorBlock, type CustomPartialBlock, type MarkdownToBlocksOptions, } from "./editor/customMarkdownConverter";
6
8
  export { useStepAutocomplete, parseStepsFromJsonApi, setStepsFetcher, type StepSuggestion, type StepJsonApiDocument, type StepJsonApiResource, } from "./editor/stepAutocomplete";
package/package/index.js CHANGED
@@ -1,6 +1,8 @@
1
1
  export { customSchema, } from "./editor/customSchema";
2
2
  export { stepBlock, canInsertStepOrSnippet, isStepsHeading, addStepsBlock, addSnippetBlock } from "./editor/blocks/step";
3
3
  export { snippetBlock } from "./editor/blocks/snippet";
4
+ export { testMetaBlock } from "./editor/blocks/testMeta";
5
+ export { setMetaFieldSuggestions, getMetaFieldSuggestions, } from "./editor/testMetaFields";
4
6
  export { markdownToHtml, htmlToMarkdown } from "./editor/blocks/markdown";
5
7
  export { blocksToMarkdown, markdownToBlocks, } from "./editor/customMarkdownConverter";
6
8
  export { useStepAutocomplete, parseStepsFromJsonApi, setStepsFetcher, } from "./editor/stepAutocomplete";
@@ -595,6 +595,207 @@ html.dark .bn-step-editor .overtype-wrapper .overtype-preview a.step-preview-lin
595
595
  flex-shrink: 0;
596
596
  }
597
597
 
598
+ /* ============================================
599
+ HEADING SIZES INSIDE THE EDITOR
600
+ Override BlockNote's default heading scale (3em / 2em / 1.3em) with a
601
+ compact 22px → 14px range. BlockNote derives heading font-size from the
602
+ `--level` custom property, so we just redefine it per level.
603
+ ============================================ */
604
+ .testomatio-editor [data-content-type="heading"] {
605
+ --level: 22px; /* h1 (level 1 has no [data-level] rule of its own) */
606
+ }
607
+ .testomatio-editor [data-content-type="heading"][data-level="2"] {
608
+ --level: 20px;
609
+ }
610
+ .testomatio-editor [data-content-type="heading"][data-level="3"] {
611
+ --level: 18px;
612
+ }
613
+ .testomatio-editor [data-content-type="heading"][data-level="4"] {
614
+ --level: 16px;
615
+ }
616
+ .testomatio-editor [data-content-type="heading"][data-level="5"] {
617
+ --level: 14px;
618
+ }
619
+ .testomatio-editor [data-content-type="heading"][data-level="6"] {
620
+ --level: 14px;
621
+ }
622
+ /* Keep size stable during BlockNote's heading-transition animation. */
623
+ .testomatio-editor [data-prev-level="1"] {
624
+ --prev-level: 22px;
625
+ }
626
+ .testomatio-editor [data-prev-level="2"] {
627
+ --prev-level: 20px;
628
+ }
629
+ .testomatio-editor [data-prev-level="3"] {
630
+ --prev-level: 18px;
631
+ }
632
+
633
+ /* ============================================
634
+ TEST / SUITE METADATA BLOCK
635
+ Dimmed card for `<!-- test ... -->` / `<!-- suite ... -->` comments.
636
+ ============================================ */
637
+ .bn-testmeta {
638
+ display: flex;
639
+ flex-direction: column;
640
+ gap: 4px;
641
+ width: 100%;
642
+ box-sizing: border-box;
643
+ padding: 6px 10px;
644
+ background: var(--bg-muted);
645
+ /*border: 1px solid var(--border-default);*/
646
+ /* Stronger top edge signals that the test case begins below this line. */
647
+ border-top: 3px solid var(--color-slate-400);
648
+ /*border-radius: 8px;*/
649
+ margin-top: 2rem;
650
+ opacity: 0.5;
651
+ }
652
+
653
+ /* Header line: `TEST @T1233456 ............ [+]` — label, id, and add button
654
+ always share one row. */
655
+ .bn-testmeta__header {
656
+ display: flex;
657
+ align-items: center;
658
+ gap: 8px;
659
+ margin-left: 8px;
660
+ }
661
+
662
+ .bn-testmeta__header .bn-testmeta__add-wrap {
663
+ margin-left: auto;
664
+ }
665
+
666
+ .bn-testmeta__label {
667
+ font-size: 11px;
668
+ font-weight: 600;
669
+ letter-spacing: 0.04em;
670
+ text-transform: uppercase;
671
+ color: var(--text-muted);
672
+ flex-shrink: 0;
673
+ }
674
+
675
+ .bn-testmeta__id {
676
+ font-size: 13px;
677
+ font-weight: 600;
678
+ color: var(--text-primary);
679
+ }
680
+
681
+ .bn-testmeta__rows {
682
+ display: flex;
683
+ flex-direction: column;
684
+ gap: 2px;
685
+ }
686
+
687
+ .bn-testmeta__row {
688
+ display: grid;
689
+ grid-template-columns: 140px minmax(0, 1fr) 24px;
690
+ align-items: center;
691
+ gap: 8px;
692
+ }
693
+
694
+ .bn-testmeta__key {
695
+ min-width: 0;
696
+ font-size: 13px;
697
+ color: var(--text-muted);
698
+ }
699
+
700
+ /* Defined values blend into the block like normal text, and only reveal the
701
+ input affordance on hover/focus ("activate on click"). */
702
+ .bn-testmeta__key--input,
703
+ .bn-testmeta__value {
704
+ width: 100%;
705
+ height: 26px;
706
+ padding: 0 8px;
707
+ box-sizing: border-box;
708
+ font-family: inherit;
709
+ font-size: 13px;
710
+ color: var(--text-primary);
711
+ background: transparent;
712
+ border: 1px solid transparent;
713
+ border-radius: 6px;
714
+ cursor: text;
715
+ }
716
+
717
+ .bn-testmeta__key--input:hover,
718
+ .bn-testmeta__value:hover {
719
+ border-color: var(--border-light);
720
+ }
721
+
722
+ .bn-testmeta__key--input:focus,
723
+ .bn-testmeta__value:focus {
724
+ outline: none;
725
+ background: var(--bg-white);
726
+ border-color: var(--step-input-border-focus);
727
+ box-shadow: 0 0 0 2px var(--step-input-shadow);
728
+ }
729
+
730
+ .bn-testmeta__remove,
731
+ .bn-testmeta__add {
732
+ width: 24px;
733
+ height: 24px;
734
+ display: inline-flex;
735
+ align-items: center;
736
+ justify-content: center;
737
+ font-size: 18px;
738
+ line-height: 1;
739
+ color: var(--text-muted);
740
+ background: transparent;
741
+ border: none;
742
+ border-radius: 6px;
743
+ cursor: pointer;
744
+ padding: 0;
745
+ }
746
+
747
+ .bn-testmeta__remove:hover,
748
+ .bn-testmeta__add:hover {
749
+ background: var(--step-bg-button-hover);
750
+ color: var(--text-primary);
751
+ }
752
+
753
+ .bn-testmeta__add-wrap {
754
+ position: relative;
755
+ }
756
+
757
+ .bn-testmeta__menu {
758
+ position: absolute;
759
+ top: calc(100% + 4px);
760
+ right: 0;
761
+ z-index: 100;
762
+ min-width: 160px;
763
+ max-height: 240px;
764
+ overflow-y: auto;
765
+ display: flex;
766
+ flex-direction: column;
767
+ padding: 4px;
768
+ background: var(--bg-white-opaque);
769
+ border: 1px solid var(--border-default);
770
+ border-radius: 8px;
771
+ box-shadow: 0 8px 24px var(--shadow-medium);
772
+ }
773
+
774
+ .bn-testmeta__menu-item {
775
+ display: block;
776
+ width: 100%;
777
+ padding: 6px 8px;
778
+ text-align: left;
779
+ font-family: inherit;
780
+ font-size: 13px;
781
+ color: var(--text-primary);
782
+ background: transparent;
783
+ border: none;
784
+ border-radius: 6px;
785
+ cursor: pointer;
786
+ }
787
+
788
+ .bn-testmeta__menu-item:hover {
789
+ background: var(--bg-muted);
790
+ }
791
+
792
+ .bn-testmeta__menu-item--custom {
793
+ margin-top: 2px;
794
+ border-top: 1px solid var(--border-light);
795
+ border-radius: 0 0 6px 6px;
796
+ color: var(--text-muted);
797
+ }
798
+
598
799
  .bn-snippet-dropdown {
599
800
  position: relative;
600
801
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testomatio-editor-blocks",
3
- "version": "0.4.65",
3
+ "version": "0.4.66",
4
4
  "description": "Custom BlockNote schema, markdown conversion helpers, and UI for Testomatio-style test cases and steps.",
5
5
  "type": "module",
6
6
  "main": "./package/index.js",