testomatio-editor-blocks 0.4.23 → 0.4.24
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/package/editor/blocks/step.d.ts +9 -0
- package/package/editor/blocks/step.js +30 -0
- package/package/editor/snippetAutocomplete.js +34 -18
- package/package/editor/stepAutocomplete.js +34 -18
- package/package/styles.css +6 -1
- package/package.json +1 -1
- package/src/App.tsx +7 -2
- package/src/editor/blocks/step.tsx +34 -0
- package/src/editor/snippetAutocomplete.ts +32 -18
- package/src/editor/stepAutocomplete.tsx +32 -18
- package/src/editor/styles.css +6 -1
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
export declare const isEmptyParagraph: (b: any) => boolean;
|
|
2
|
+
/**
|
|
3
|
+
* Check whether a step or snippet can be inserted at / after the given block.
|
|
4
|
+
* Returns true only when walking backwards from `referenceBlockId` (skipping
|
|
5
|
+
* other steps, snippets, and empty paragraphs) reaches a heading whose text
|
|
6
|
+
* is "steps".
|
|
7
|
+
*/
|
|
8
|
+
export declare function canInsertStepOrSnippet(editor: {
|
|
9
|
+
document: any[];
|
|
10
|
+
}, referenceBlockId: string): boolean;
|
|
2
11
|
export declare const stepBlock: {
|
|
3
12
|
config: {
|
|
4
13
|
readonly type: "testStep";
|
|
@@ -47,6 +47,36 @@ export const isEmptyParagraph = (b) => b.type === "paragraph" &&
|
|
|
47
47
|
(!Array.isArray(b.content) ||
|
|
48
48
|
b.content.length === 0 ||
|
|
49
49
|
b.content.every((n) => { var _a; return n.type === "text" && !((_a = n.text) === null || _a === void 0 ? void 0 : _a.trim()); }));
|
|
50
|
+
/**
|
|
51
|
+
* Check whether a step or snippet can be inserted at / after the given block.
|
|
52
|
+
* Returns true only when walking backwards from `referenceBlockId` (skipping
|
|
53
|
+
* other steps, snippets, and empty paragraphs) reaches a heading whose text
|
|
54
|
+
* is "steps".
|
|
55
|
+
*/
|
|
56
|
+
export function canInsertStepOrSnippet(editor, referenceBlockId) {
|
|
57
|
+
const allBlocks = editor.document;
|
|
58
|
+
const blockIndex = allBlocks.findIndex((b) => b.id === referenceBlockId);
|
|
59
|
+
if (blockIndex < 0)
|
|
60
|
+
return false;
|
|
61
|
+
for (let i = blockIndex; i >= 0; i--) {
|
|
62
|
+
const b = allBlocks[i];
|
|
63
|
+
if (b.type === "testStep" || b.type === "snippet" || isEmptyParagraph(b)) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (b.type === "heading") {
|
|
67
|
+
const text = (Array.isArray(b.content) ? b.content : [])
|
|
68
|
+
.filter((n) => n.type === "text")
|
|
69
|
+
.map((n) => { var _a; return (_a = n.text) !== null && _a !== void 0 ? _a : ""; })
|
|
70
|
+
.join("")
|
|
71
|
+
.trim()
|
|
72
|
+
.toLowerCase()
|
|
73
|
+
.replace(/[:\-–—]$/, "");
|
|
74
|
+
return text === "steps";
|
|
75
|
+
}
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
50
80
|
export const stepBlock = createReactBlockSpec({
|
|
51
81
|
type: "testStep",
|
|
52
82
|
content: "none",
|
|
@@ -1,24 +1,31 @@
|
|
|
1
1
|
import { useEffect, useState } from "react";
|
|
2
2
|
let globalFetcher = null;
|
|
3
3
|
let cachedSuggestions = [];
|
|
4
|
+
let inflightPromise = null;
|
|
4
5
|
export function setSnippetFetcher(fetcher) {
|
|
5
6
|
globalFetcher = fetcher;
|
|
6
7
|
cachedSuggestions = [];
|
|
8
|
+
inflightPromise = null;
|
|
7
9
|
}
|
|
8
10
|
export function useSnippetAutocomplete() {
|
|
9
11
|
const [suggestions, setSuggestions] = useState(() => {
|
|
10
|
-
if (cachedSuggestions.length > 0)
|
|
12
|
+
if (cachedSuggestions.length > 0)
|
|
11
13
|
return cachedSuggestions;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
if (!globalFetcher)
|
|
15
|
+
return [];
|
|
16
|
+
const result = globalFetcher();
|
|
17
|
+
if (result && typeof result.then === "function") {
|
|
18
|
+
if (!inflightPromise) {
|
|
19
|
+
inflightPromise = result
|
|
20
|
+
.then((r) => normalizeSnippetSuggestions(r))
|
|
21
|
+
.then((items) => { cachedSuggestions = items; inflightPromise = null; return items; })
|
|
22
|
+
.catch((error) => { inflightPromise = null; console.error("Failed to fetch snippet suggestions", error); return []; });
|
|
19
23
|
}
|
|
24
|
+
return [];
|
|
20
25
|
}
|
|
21
|
-
|
|
26
|
+
const normalized = normalizeSnippetSuggestions(result);
|
|
27
|
+
cachedSuggestions = normalized;
|
|
28
|
+
return normalized;
|
|
22
29
|
});
|
|
23
30
|
useEffect(() => {
|
|
24
31
|
if (suggestions.length > 0) {
|
|
@@ -28,15 +35,24 @@ export function useSnippetAutocomplete() {
|
|
|
28
35
|
return;
|
|
29
36
|
}
|
|
30
37
|
let cancelled = false;
|
|
31
|
-
|
|
32
|
-
.
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
if (!inflightPromise) {
|
|
39
|
+
inflightPromise = Promise.resolve(globalFetcher())
|
|
40
|
+
.then((result) => normalizeSnippetSuggestions(result))
|
|
41
|
+
.then((items) => {
|
|
42
|
+
cachedSuggestions = items;
|
|
43
|
+
inflightPromise = null;
|
|
44
|
+
return items;
|
|
45
|
+
})
|
|
46
|
+
.catch((error) => {
|
|
47
|
+
inflightPromise = null;
|
|
48
|
+
console.error("Failed to fetch snippet suggestions", error);
|
|
49
|
+
return [];
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
inflightPromise.then((items) => {
|
|
53
|
+
if (!cancelled)
|
|
54
|
+
setSuggestions(items);
|
|
55
|
+
});
|
|
40
56
|
return () => {
|
|
41
57
|
cancelled = true;
|
|
42
58
|
};
|
|
@@ -1,24 +1,31 @@
|
|
|
1
1
|
import { useEffect, useState } from "react";
|
|
2
2
|
let globalFetcher = null;
|
|
3
3
|
let cachedSuggestions = [];
|
|
4
|
+
let inflightPromise = null;
|
|
4
5
|
export function setStepsFetcher(fetcher) {
|
|
5
6
|
globalFetcher = fetcher;
|
|
6
7
|
cachedSuggestions = [];
|
|
8
|
+
inflightPromise = null;
|
|
7
9
|
}
|
|
8
10
|
export function useStepAutocomplete() {
|
|
9
11
|
const [suggestions, setSuggestions] = useState(() => {
|
|
10
|
-
if (cachedSuggestions.length > 0)
|
|
12
|
+
if (cachedSuggestions.length > 0)
|
|
11
13
|
return cachedSuggestions;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
if (!globalFetcher)
|
|
15
|
+
return [];
|
|
16
|
+
const result = globalFetcher();
|
|
17
|
+
if (result && typeof result.then === "function") {
|
|
18
|
+
if (!inflightPromise) {
|
|
19
|
+
inflightPromise = result
|
|
20
|
+
.then((r) => normalizeStepSuggestions(r))
|
|
21
|
+
.then((items) => { cachedSuggestions = items; inflightPromise = null; return items; })
|
|
22
|
+
.catch((error) => { inflightPromise = null; console.error("Failed to fetch step suggestions", error); return []; });
|
|
19
23
|
}
|
|
24
|
+
return [];
|
|
20
25
|
}
|
|
21
|
-
|
|
26
|
+
const normalized = normalizeStepSuggestions(result);
|
|
27
|
+
cachedSuggestions = normalized;
|
|
28
|
+
return normalized;
|
|
22
29
|
});
|
|
23
30
|
useEffect(() => {
|
|
24
31
|
if (suggestions.length > 0) {
|
|
@@ -28,15 +35,24 @@ export function useStepAutocomplete() {
|
|
|
28
35
|
return;
|
|
29
36
|
}
|
|
30
37
|
let cancelled = false;
|
|
31
|
-
|
|
32
|
-
.
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
if (!inflightPromise) {
|
|
39
|
+
inflightPromise = Promise.resolve(globalFetcher())
|
|
40
|
+
.then((result) => normalizeStepSuggestions(result))
|
|
41
|
+
.then((items) => {
|
|
42
|
+
cachedSuggestions = items;
|
|
43
|
+
inflightPromise = null;
|
|
44
|
+
return items;
|
|
45
|
+
})
|
|
46
|
+
.catch((error) => {
|
|
47
|
+
inflightPromise = null;
|
|
48
|
+
console.error("Failed to fetch step suggestions", error);
|
|
49
|
+
return [];
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
inflightPromise.then((items) => {
|
|
53
|
+
if (!cancelled)
|
|
54
|
+
setSuggestions(items);
|
|
55
|
+
});
|
|
40
56
|
return () => {
|
|
41
57
|
cancelled = true;
|
|
42
58
|
};
|
package/package/styles.css
CHANGED
|
@@ -515,7 +515,6 @@ html.dark .bn-step-image-preview__content {
|
|
|
515
515
|
display: flex;
|
|
516
516
|
flex-direction: column;
|
|
517
517
|
gap: 4px;
|
|
518
|
-
overflow: hidden;
|
|
519
518
|
}
|
|
520
519
|
|
|
521
520
|
.bn-teststep__horizontal-col .bn-step-field__top {
|
|
@@ -1212,6 +1211,12 @@ html.dark .bn-step-image-preview__content {
|
|
|
1212
1211
|
border-radius: 999px;
|
|
1213
1212
|
width: 1.35rem;
|
|
1214
1213
|
height: 1.35rem;
|
|
1214
|
+
display: flex;
|
|
1215
|
+
align-items: center;
|
|
1216
|
+
justify-content: center;
|
|
1217
|
+
padding: 0;
|
|
1218
|
+
font-size: 0.85rem;
|
|
1219
|
+
line-height: 1;
|
|
1215
1220
|
background: var(--overlay-dark);
|
|
1216
1221
|
color: var(--color-white);
|
|
1217
1222
|
cursor: pointer;
|
package/package.json
CHANGED
package/src/App.tsx
CHANGED
|
@@ -21,6 +21,7 @@ import { customSchema, type CustomEditor } from "./editor/customSchema";
|
|
|
21
21
|
import { setStepsFetcher, type StepJsonApiDocument } from "./editor/stepAutocomplete";
|
|
22
22
|
import { setSnippetFetcher, type SnippetJsonApiDocument } from "./editor/snippetAutocomplete";
|
|
23
23
|
import { setImageUploadHandler } from "./editor/stepImageUpload";
|
|
24
|
+
import { canInsertStepOrSnippet } from "./editor/blocks/step";
|
|
24
25
|
import "./App.css";
|
|
25
26
|
|
|
26
27
|
const focusStepField = (
|
|
@@ -333,7 +334,11 @@ function CustomSlashMenu() {
|
|
|
333
334
|
},
|
|
334
335
|
};
|
|
335
336
|
|
|
336
|
-
|
|
337
|
+
const cursorBlock = editor.getTextCursorPosition()?.block;
|
|
338
|
+
const canInsert = cursorBlock ? canInsertStepOrSnippet(editor, cursorBlock.id) : false;
|
|
339
|
+
const customItems = canInsert ? [stepItem, snippetItem] : [];
|
|
340
|
+
|
|
341
|
+
return filterSuggestionItems([...defaultItems, ...customItems], query);
|
|
337
342
|
};
|
|
338
343
|
|
|
339
344
|
return <SuggestionMenuController triggerCharacter="/" getItems={getItems} />;
|
|
@@ -519,7 +524,7 @@ function App() {
|
|
|
519
524
|
const fallbackBlock = documentBlocks[documentBlocks.length - 1];
|
|
520
525
|
const referenceId = selectedBlock?.id ?? fallbackBlock?.id;
|
|
521
526
|
|
|
522
|
-
if (!referenceId) {
|
|
527
|
+
if (!referenceId || !canInsertStepOrSnippet(editor, referenceId)) {
|
|
523
528
|
return;
|
|
524
529
|
}
|
|
525
530
|
|
|
@@ -53,6 +53,40 @@ export const isEmptyParagraph = (b: any): boolean =>
|
|
|
53
53
|
b.content.length === 0 ||
|
|
54
54
|
b.content.every((n: any) => n.type === "text" && !n.text?.trim()));
|
|
55
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Check whether a step or snippet can be inserted at / after the given block.
|
|
58
|
+
* Returns true only when walking backwards from `referenceBlockId` (skipping
|
|
59
|
+
* other steps, snippets, and empty paragraphs) reaches a heading whose text
|
|
60
|
+
* is "steps".
|
|
61
|
+
*/
|
|
62
|
+
export function canInsertStepOrSnippet(
|
|
63
|
+
editor: { document: any[] },
|
|
64
|
+
referenceBlockId: string,
|
|
65
|
+
): boolean {
|
|
66
|
+
const allBlocks = editor.document;
|
|
67
|
+
const blockIndex = allBlocks.findIndex((b: any) => b.id === referenceBlockId);
|
|
68
|
+
if (blockIndex < 0) return false;
|
|
69
|
+
|
|
70
|
+
for (let i = blockIndex; i >= 0; i--) {
|
|
71
|
+
const b = allBlocks[i];
|
|
72
|
+
if (b.type === "testStep" || b.type === "snippet" || isEmptyParagraph(b)) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (b.type === "heading") {
|
|
76
|
+
const text = (Array.isArray(b.content) ? b.content : [])
|
|
77
|
+
.filter((n: any) => n.type === "text")
|
|
78
|
+
.map((n: any) => n.text ?? "")
|
|
79
|
+
.join("")
|
|
80
|
+
.trim()
|
|
81
|
+
.toLowerCase()
|
|
82
|
+
.replace(/[:\-–—]$/, "");
|
|
83
|
+
return text === "steps";
|
|
84
|
+
}
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
|
|
56
90
|
export const stepBlock = createReactBlockSpec(
|
|
57
91
|
{
|
|
58
92
|
type: "testStep",
|
|
@@ -33,26 +33,31 @@ type SnippetInput = SnippetSuggestion[] | SnippetJsonApiDocument | SnippetJsonAp
|
|
|
33
33
|
|
|
34
34
|
let globalFetcher: SnippetSuggestionsFetcher | null = null;
|
|
35
35
|
let cachedSuggestions: SnippetSuggestion[] = [];
|
|
36
|
+
let inflightPromise: Promise<SnippetSuggestion[]> | null = null;
|
|
36
37
|
|
|
37
38
|
export function setSnippetFetcher(fetcher: SnippetSuggestionsFetcher | null) {
|
|
38
39
|
globalFetcher = fetcher;
|
|
39
40
|
cachedSuggestions = [];
|
|
41
|
+
inflightPromise = null;
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
export function useSnippetAutocomplete(): SnippetSuggestion[] {
|
|
43
45
|
const [suggestions, setSuggestions] = useState<SnippetSuggestion[]>(() => {
|
|
44
|
-
if (cachedSuggestions.length > 0)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
if (
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
46
|
+
if (cachedSuggestions.length > 0) return cachedSuggestions;
|
|
47
|
+
if (!globalFetcher) return [];
|
|
48
|
+
const result = globalFetcher();
|
|
49
|
+
if (result && typeof (result as Promise<unknown>).then === "function") {
|
|
50
|
+
if (!inflightPromise) {
|
|
51
|
+
inflightPromise = (result as Promise<SnippetInput>)
|
|
52
|
+
.then((r) => normalizeSnippetSuggestions(r))
|
|
53
|
+
.then((items) => { cachedSuggestions = items; inflightPromise = null; return items; })
|
|
54
|
+
.catch((error) => { inflightPromise = null; console.error("Failed to fetch snippet suggestions", error); return [] as SnippetSuggestion[]; });
|
|
53
55
|
}
|
|
56
|
+
return [];
|
|
54
57
|
}
|
|
55
|
-
|
|
58
|
+
const normalized = normalizeSnippetSuggestions(result as SnippetInput);
|
|
59
|
+
cachedSuggestions = normalized;
|
|
60
|
+
return normalized;
|
|
56
61
|
});
|
|
57
62
|
|
|
58
63
|
useEffect(() => {
|
|
@@ -64,14 +69,23 @@ export function useSnippetAutocomplete(): SnippetSuggestion[] {
|
|
|
64
69
|
}
|
|
65
70
|
|
|
66
71
|
let cancelled = false;
|
|
67
|
-
|
|
68
|
-
.
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
72
|
+
if (!inflightPromise) {
|
|
73
|
+
inflightPromise = Promise.resolve(globalFetcher())
|
|
74
|
+
.then((result) => normalizeSnippetSuggestions(result))
|
|
75
|
+
.then((items) => {
|
|
76
|
+
cachedSuggestions = items;
|
|
77
|
+
inflightPromise = null;
|
|
78
|
+
return items;
|
|
79
|
+
})
|
|
80
|
+
.catch((error) => {
|
|
81
|
+
inflightPromise = null;
|
|
82
|
+
console.error("Failed to fetch snippet suggestions", error);
|
|
83
|
+
return [] as SnippetSuggestion[];
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
inflightPromise.then((items) => {
|
|
87
|
+
if (!cancelled) setSuggestions(items);
|
|
88
|
+
});
|
|
75
89
|
|
|
76
90
|
return () => {
|
|
77
91
|
cancelled = true;
|
|
@@ -39,26 +39,31 @@ type StepInput = StepSuggestion[] | StepJsonApiDocument | StepJsonApiResource[]
|
|
|
39
39
|
|
|
40
40
|
let globalFetcher: StepSuggestionsFetcher | null = null;
|
|
41
41
|
let cachedSuggestions: StepSuggestion[] = [];
|
|
42
|
+
let inflightPromise: Promise<StepSuggestion[]> | null = null;
|
|
42
43
|
|
|
43
44
|
export function setStepsFetcher(fetcher: StepSuggestionsFetcher | null) {
|
|
44
45
|
globalFetcher = fetcher;
|
|
45
46
|
cachedSuggestions = [];
|
|
47
|
+
inflightPromise = null;
|
|
46
48
|
}
|
|
47
49
|
|
|
48
50
|
export function useStepAutocomplete(): StepSuggestion[] {
|
|
49
51
|
const [suggestions, setSuggestions] = useState<StepSuggestion[]>(() => {
|
|
50
|
-
if (cachedSuggestions.length > 0)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if (
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
52
|
+
if (cachedSuggestions.length > 0) return cachedSuggestions;
|
|
53
|
+
if (!globalFetcher) return [];
|
|
54
|
+
const result = globalFetcher();
|
|
55
|
+
if (result && typeof (result as Promise<unknown>).then === "function") {
|
|
56
|
+
if (!inflightPromise) {
|
|
57
|
+
inflightPromise = (result as Promise<StepInput>)
|
|
58
|
+
.then((r) => normalizeStepSuggestions(r))
|
|
59
|
+
.then((items) => { cachedSuggestions = items; inflightPromise = null; return items; })
|
|
60
|
+
.catch((error) => { inflightPromise = null; console.error("Failed to fetch step suggestions", error); return [] as StepSuggestion[]; });
|
|
59
61
|
}
|
|
62
|
+
return [];
|
|
60
63
|
}
|
|
61
|
-
|
|
64
|
+
const normalized = normalizeStepSuggestions(result as StepInput);
|
|
65
|
+
cachedSuggestions = normalized;
|
|
66
|
+
return normalized;
|
|
62
67
|
});
|
|
63
68
|
|
|
64
69
|
useEffect(() => {
|
|
@@ -70,14 +75,23 @@ export function useStepAutocomplete(): StepSuggestion[] {
|
|
|
70
75
|
}
|
|
71
76
|
|
|
72
77
|
let cancelled = false;
|
|
73
|
-
|
|
74
|
-
.
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
78
|
+
if (!inflightPromise) {
|
|
79
|
+
inflightPromise = Promise.resolve(globalFetcher())
|
|
80
|
+
.then((result) => normalizeStepSuggestions(result))
|
|
81
|
+
.then((items) => {
|
|
82
|
+
cachedSuggestions = items;
|
|
83
|
+
inflightPromise = null;
|
|
84
|
+
return items;
|
|
85
|
+
})
|
|
86
|
+
.catch((error) => {
|
|
87
|
+
inflightPromise = null;
|
|
88
|
+
console.error("Failed to fetch step suggestions", error);
|
|
89
|
+
return [] as StepSuggestion[];
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
inflightPromise.then((items) => {
|
|
93
|
+
if (!cancelled) setSuggestions(items);
|
|
94
|
+
});
|
|
81
95
|
|
|
82
96
|
return () => {
|
|
83
97
|
cancelled = true;
|
package/src/editor/styles.css
CHANGED
|
@@ -515,7 +515,6 @@ html.dark .bn-step-image-preview__content {
|
|
|
515
515
|
display: flex;
|
|
516
516
|
flex-direction: column;
|
|
517
517
|
gap: 4px;
|
|
518
|
-
overflow: hidden;
|
|
519
518
|
}
|
|
520
519
|
|
|
521
520
|
.bn-teststep__horizontal-col .bn-step-field__top {
|
|
@@ -1212,6 +1211,12 @@ html.dark .bn-step-image-preview__content {
|
|
|
1212
1211
|
border-radius: 999px;
|
|
1213
1212
|
width: 1.35rem;
|
|
1214
1213
|
height: 1.35rem;
|
|
1214
|
+
display: flex;
|
|
1215
|
+
align-items: center;
|
|
1216
|
+
justify-content: center;
|
|
1217
|
+
padding: 0;
|
|
1218
|
+
font-size: 0.85rem;
|
|
1219
|
+
line-height: 1;
|
|
1215
1220
|
background: var(--overlay-dark);
|
|
1216
1221
|
color: var(--color-white);
|
|
1217
1222
|
cursor: pointer;
|