@ekairos/toolbar 1.22.4-beta.development.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 +43 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +4 -0
- package/dist/output.d.ts +2 -0
- package/dist/output.js +37 -0
- package/dist/popup.d.ts +17 -0
- package/dist/popup.js +108 -0
- package/dist/selectors.d.ts +8 -0
- package/dist/selectors.js +216 -0
- package/dist/toolbar.d.ts +2 -0
- package/dist/toolbar.js +898 -0
- package/dist/types.d.ts +54 -0
- package/dist/types.js +1 -0
- package/package.json +45 -0
package/README.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# @ekairos/toolbar
|
|
2
|
+
|
|
3
|
+
Lightweight visual feedback toolbar for selecting UI elements, collecting annotations, and exporting structured feedback.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @ekairos/toolbar
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
import { EkairosToolbar } from "@ekairos/toolbar";
|
|
15
|
+
|
|
16
|
+
export function App() {
|
|
17
|
+
return (
|
|
18
|
+
<>
|
|
19
|
+
<YourApp />
|
|
20
|
+
<EkairosToolbar />
|
|
21
|
+
</>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Included
|
|
27
|
+
|
|
28
|
+
- Single element selection
|
|
29
|
+
- Multi-select by `Cmd/Ctrl + Shift + Click`
|
|
30
|
+
- Drag multi-select and area selection
|
|
31
|
+
- Feedback dialog (add/edit/delete)
|
|
32
|
+
- Stable selector extraction (`stableSelector`) plus readable path (`elementPath`)
|
|
33
|
+
- Markdown output generation and copy/send callbacks
|
|
34
|
+
|
|
35
|
+
## Keyboard
|
|
36
|
+
|
|
37
|
+
- `Cmd/Ctrl + Shift + F`: toggle feedback mode
|
|
38
|
+
- `Esc`: cancel current interaction / close
|
|
39
|
+
- `C`: copy output
|
|
40
|
+
- `S`: send output callback
|
|
41
|
+
- `X`: clear annotations
|
|
42
|
+
- `H`: show/hide markers
|
|
43
|
+
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { EkairosToolbar } from "./toolbar";
|
|
2
|
+
export { ToolbarPopup } from "./popup";
|
|
3
|
+
export { getElementPath, getStableSelector, identifyElementName, getElementClasses, getNearbyText, deepElementFromPoint, closestCrossingShadow, isElementFixed, } from "./selectors";
|
|
4
|
+
export { generateToolbarOutput } from "./output";
|
|
5
|
+
export type { BoundingBox, EkairosToolbarProps, OutputDetailLevel, ToolbarAnnotation, ToolbarSelectionSnapshot, } from "./types";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { EkairosToolbar } from "./toolbar";
|
|
2
|
+
export { ToolbarPopup } from "./popup";
|
|
3
|
+
export { getElementPath, getStableSelector, identifyElementName, getElementClasses, getNearbyText, deepElementFromPoint, closestCrossingShadow, isElementFixed, } from "./selectors";
|
|
4
|
+
export { generateToolbarOutput } from "./output";
|
package/dist/output.d.ts
ADDED
package/dist/output.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export function generateToolbarOutput(annotations, pagePath, detail = "standard") {
|
|
2
|
+
if (annotations.length === 0)
|
|
3
|
+
return "";
|
|
4
|
+
const viewport = typeof window !== "undefined"
|
|
5
|
+
? `${window.innerWidth}x${window.innerHeight}`
|
|
6
|
+
: "unknown";
|
|
7
|
+
let output = `## Toolbar Feedback: ${pagePath}\n`;
|
|
8
|
+
output += `**Viewport:** ${viewport}\n\n`;
|
|
9
|
+
annotations.forEach((annotation, index) => {
|
|
10
|
+
if (detail === "compact") {
|
|
11
|
+
output += `${index + 1}. ${annotation.element}: ${annotation.comment}\n`;
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
output += `### ${index + 1}. ${annotation.element}\n`;
|
|
15
|
+
output += `- Feedback: ${annotation.comment}\n`;
|
|
16
|
+
output += `- Path: ${annotation.elementPath}\n`;
|
|
17
|
+
if (annotation.stableSelector) {
|
|
18
|
+
output += `- Selector: \`${annotation.stableSelector}\`\n`;
|
|
19
|
+
}
|
|
20
|
+
if (annotation.selectedText) {
|
|
21
|
+
output += `- Selected text: "${annotation.selectedText}"\n`;
|
|
22
|
+
}
|
|
23
|
+
if (detail === "detailed") {
|
|
24
|
+
if (annotation.cssClasses) {
|
|
25
|
+
output += `- Classes: ${annotation.cssClasses}\n`;
|
|
26
|
+
}
|
|
27
|
+
if (annotation.boundingBox) {
|
|
28
|
+
output += `- Box: x=${Math.round(annotation.boundingBox.x)}, y=${Math.round(annotation.boundingBox.y)}, w=${Math.round(annotation.boundingBox.width)}, h=${Math.round(annotation.boundingBox.height)}\n`;
|
|
29
|
+
}
|
|
30
|
+
if (annotation.nearbyText) {
|
|
31
|
+
output += `- Context: ${annotation.nearbyText.slice(0, 120)}\n`;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
output += "\n";
|
|
35
|
+
});
|
|
36
|
+
return output.trim();
|
|
37
|
+
}
|
package/dist/popup.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type CSSProperties } from "react";
|
|
2
|
+
export type ToolbarPopupProps = {
|
|
3
|
+
element: string;
|
|
4
|
+
selectedText?: string;
|
|
5
|
+
placeholder?: string;
|
|
6
|
+
initialValue?: string;
|
|
7
|
+
submitLabel?: string;
|
|
8
|
+
onSubmit: (text: string) => void;
|
|
9
|
+
onCancel: () => void;
|
|
10
|
+
onDelete?: () => void;
|
|
11
|
+
style?: CSSProperties;
|
|
12
|
+
accentColor?: string;
|
|
13
|
+
};
|
|
14
|
+
export type ToolbarPopupHandle = {
|
|
15
|
+
shake: () => void;
|
|
16
|
+
};
|
|
17
|
+
export declare const ToolbarPopup: import("react").ForwardRefExoticComponent<ToolbarPopupProps & import("react").RefAttributes<ToolbarPopupHandle>>;
|
package/dist/popup.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState, } from "react";
|
|
3
|
+
export const ToolbarPopup = forwardRef(function ToolbarPopup({ element, selectedText, placeholder = "What should change?", initialValue = "", submitLabel = "Add", onSubmit, onCancel, onDelete, style, accentColor = "#2f7bf6", }, ref) {
|
|
4
|
+
const [text, setText] = useState(initialValue);
|
|
5
|
+
const [isFocused, setIsFocused] = useState(false);
|
|
6
|
+
const [isShaking, setIsShaking] = useState(false);
|
|
7
|
+
const textareaRef = useRef(null);
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
setText(initialValue);
|
|
10
|
+
}, [initialValue]);
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
const timer = setTimeout(() => {
|
|
13
|
+
textareaRef.current?.focus();
|
|
14
|
+
}, 40);
|
|
15
|
+
return () => clearTimeout(timer);
|
|
16
|
+
}, []);
|
|
17
|
+
const shake = useCallback(() => {
|
|
18
|
+
setIsShaking(true);
|
|
19
|
+
const timer = setTimeout(() => setIsShaking(false), 250);
|
|
20
|
+
return () => clearTimeout(timer);
|
|
21
|
+
}, []);
|
|
22
|
+
useImperativeHandle(ref, () => ({ shake }), [shake]);
|
|
23
|
+
const submit = useCallback(() => {
|
|
24
|
+
const value = text.trim();
|
|
25
|
+
if (!value)
|
|
26
|
+
return;
|
|
27
|
+
onSubmit(value);
|
|
28
|
+
}, [text, onSubmit]);
|
|
29
|
+
const cancel = useCallback(() => {
|
|
30
|
+
onCancel();
|
|
31
|
+
}, [onCancel]);
|
|
32
|
+
return (_jsxs("div", { "data-ekairos-toolbar-popup": true, onClick: (event) => event.stopPropagation(), style: {
|
|
33
|
+
position: "fixed",
|
|
34
|
+
width: 300,
|
|
35
|
+
borderRadius: 12,
|
|
36
|
+
border: `1px solid ${isShaking ? "#f14668" : "rgba(255,255,255,0.08)"}`,
|
|
37
|
+
background: "#16161b",
|
|
38
|
+
color: "#f5f7fa",
|
|
39
|
+
padding: 12,
|
|
40
|
+
zIndex: 100003,
|
|
41
|
+
boxShadow: "0 10px 35px rgba(0,0,0,0.45)",
|
|
42
|
+
...style,
|
|
43
|
+
}, children: [_jsx("div", { style: {
|
|
44
|
+
fontSize: 12,
|
|
45
|
+
color: "rgba(255,255,255,0.65)",
|
|
46
|
+
marginBottom: 8,
|
|
47
|
+
whiteSpace: "nowrap",
|
|
48
|
+
overflow: "hidden",
|
|
49
|
+
textOverflow: "ellipsis",
|
|
50
|
+
}, children: element }), selectedText ? (_jsxs("div", { style: {
|
|
51
|
+
fontSize: 12,
|
|
52
|
+
color: "rgba(255,255,255,0.65)",
|
|
53
|
+
background: "rgba(255,255,255,0.05)",
|
|
54
|
+
borderRadius: 8,
|
|
55
|
+
padding: "6px 8px",
|
|
56
|
+
marginBottom: 8,
|
|
57
|
+
}, children: ["\"", selectedText.slice(0, 100), selectedText.length > 100 ? "..." : "", "\""] })) : null, _jsx("textarea", { ref: textareaRef, value: text, rows: 3, placeholder: placeholder, onChange: (event) => setText(event.target.value), onFocus: () => setIsFocused(true), onBlur: () => setIsFocused(false), onKeyDown: (event) => {
|
|
58
|
+
if (event.key === "Escape") {
|
|
59
|
+
event.preventDefault();
|
|
60
|
+
cancel();
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (event.key === "Enter" && !event.shiftKey && !event.nativeEvent.isComposing) {
|
|
64
|
+
event.preventDefault();
|
|
65
|
+
submit();
|
|
66
|
+
}
|
|
67
|
+
}, style: {
|
|
68
|
+
width: "100%",
|
|
69
|
+
resize: "none",
|
|
70
|
+
borderRadius: 8,
|
|
71
|
+
border: `1px solid ${isFocused ? accentColor : "rgba(255,255,255,0.15)"}`,
|
|
72
|
+
background: "rgba(255,255,255,0.04)",
|
|
73
|
+
color: "#f5f7fa",
|
|
74
|
+
fontSize: 13,
|
|
75
|
+
lineHeight: 1.4,
|
|
76
|
+
padding: "8px 10px",
|
|
77
|
+
outline: "none",
|
|
78
|
+
boxSizing: "border-box",
|
|
79
|
+
} }), _jsxs("div", { style: {
|
|
80
|
+
display: "flex",
|
|
81
|
+
alignItems: "center",
|
|
82
|
+
gap: 8,
|
|
83
|
+
marginTop: 8,
|
|
84
|
+
}, children: [onDelete ? (_jsx("button", { type: "button", onClick: onDelete, style: {
|
|
85
|
+
marginRight: "auto",
|
|
86
|
+
border: "none",
|
|
87
|
+
background: "transparent",
|
|
88
|
+
color: "rgba(255,255,255,0.7)",
|
|
89
|
+
cursor: "pointer",
|
|
90
|
+
padding: "4px 6px",
|
|
91
|
+
}, children: "Delete" })) : null, _jsx("button", { type: "button", onClick: cancel, style: {
|
|
92
|
+
border: "none",
|
|
93
|
+
background: "transparent",
|
|
94
|
+
color: "rgba(255,255,255,0.72)",
|
|
95
|
+
cursor: "pointer",
|
|
96
|
+
padding: "4px 8px",
|
|
97
|
+
}, children: "Cancel" }), _jsx("button", { type: "button", onClick: submit, disabled: !text.trim(), style: {
|
|
98
|
+
border: "none",
|
|
99
|
+
borderRadius: 999,
|
|
100
|
+
background: accentColor,
|
|
101
|
+
color: "#fff",
|
|
102
|
+
cursor: text.trim() ? "pointer" : "not-allowed",
|
|
103
|
+
opacity: text.trim() ? 1 : 0.45,
|
|
104
|
+
padding: "5px 12px",
|
|
105
|
+
fontSize: 12,
|
|
106
|
+
fontWeight: 600,
|
|
107
|
+
}, children: submitLabel })] })] }));
|
|
108
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare function deepElementFromPoint(x: number, y: number): HTMLElement | null;
|
|
2
|
+
export declare function closestCrossingShadow(element: Element, selector: string): Element | null;
|
|
3
|
+
export declare function isElementFixed(element: HTMLElement): boolean;
|
|
4
|
+
export declare function getElementPath(target: HTMLElement, maxDepth?: number): string;
|
|
5
|
+
export declare function getStableSelector(target: HTMLElement): string;
|
|
6
|
+
export declare function identifyElementName(target: HTMLElement): string;
|
|
7
|
+
export declare function getElementClasses(target: HTMLElement): string;
|
|
8
|
+
export declare function getNearbyText(target: HTMLElement): string;
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
const TEST_ID_ATTRS = [
|
|
2
|
+
"data-ekairos-id",
|
|
3
|
+
"data-testid",
|
|
4
|
+
"data-test",
|
|
5
|
+
"data-qa",
|
|
6
|
+
"data-cy",
|
|
7
|
+
"data-automation-id",
|
|
8
|
+
];
|
|
9
|
+
function getParentElementCrossShadow(element) {
|
|
10
|
+
if (element.parentElement)
|
|
11
|
+
return element.parentElement;
|
|
12
|
+
const root = element.getRootNode();
|
|
13
|
+
if (root instanceof ShadowRoot)
|
|
14
|
+
return root.host;
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
function escapeCssValue(value) {
|
|
18
|
+
if (typeof CSS !== "undefined" && typeof CSS.escape === "function") {
|
|
19
|
+
return CSS.escape(value);
|
|
20
|
+
}
|
|
21
|
+
return value.replace(/["\\]/g, "\\$&");
|
|
22
|
+
}
|
|
23
|
+
function isUniqueSelector(selector) {
|
|
24
|
+
try {
|
|
25
|
+
return document.querySelectorAll(selector).length === 1;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function getMeaningfulClassList(element) {
|
|
32
|
+
return Array.from(element.classList)
|
|
33
|
+
.map((cls) => cls.trim())
|
|
34
|
+
.filter((cls) => cls.length > 2)
|
|
35
|
+
.filter((cls) => !/^[a-z]{1,2}$/.test(cls))
|
|
36
|
+
.filter((cls) => !/[A-Z0-9]{6,}/.test(cls));
|
|
37
|
+
}
|
|
38
|
+
function getNthOfTypeIndex(element) {
|
|
39
|
+
const parent = element.parentElement;
|
|
40
|
+
if (!parent)
|
|
41
|
+
return 1;
|
|
42
|
+
const sameTagSiblings = Array.from(parent.children).filter((child) => child.tagName === element.tagName);
|
|
43
|
+
return sameTagSiblings.indexOf(element) + 1;
|
|
44
|
+
}
|
|
45
|
+
export function deepElementFromPoint(x, y) {
|
|
46
|
+
let current = document.elementFromPoint(x, y);
|
|
47
|
+
if (!current)
|
|
48
|
+
return null;
|
|
49
|
+
while (current.shadowRoot) {
|
|
50
|
+
const deeper = current.shadowRoot.elementFromPoint(x, y);
|
|
51
|
+
if (!deeper || deeper === current)
|
|
52
|
+
break;
|
|
53
|
+
current = deeper;
|
|
54
|
+
}
|
|
55
|
+
return current;
|
|
56
|
+
}
|
|
57
|
+
export function closestCrossingShadow(element, selector) {
|
|
58
|
+
let current = element;
|
|
59
|
+
while (current) {
|
|
60
|
+
if (current.matches(selector))
|
|
61
|
+
return current;
|
|
62
|
+
current = getParentElementCrossShadow(current);
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
export function isElementFixed(element) {
|
|
67
|
+
let current = element;
|
|
68
|
+
while (current && current !== document.body) {
|
|
69
|
+
const position = window.getComputedStyle(current).position;
|
|
70
|
+
if (position === "fixed" || position === "sticky")
|
|
71
|
+
return true;
|
|
72
|
+
current = current.parentElement;
|
|
73
|
+
}
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
export function getElementPath(target, maxDepth = 5) {
|
|
77
|
+
const parts = [];
|
|
78
|
+
let current = target;
|
|
79
|
+
let depth = 0;
|
|
80
|
+
while (current && depth < maxDepth) {
|
|
81
|
+
const tag = current.tagName.toLowerCase();
|
|
82
|
+
if (tag === "html" || tag === "body")
|
|
83
|
+
break;
|
|
84
|
+
let segment = tag;
|
|
85
|
+
if (current.id) {
|
|
86
|
+
segment = `#${current.id}`;
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
const classes = getMeaningfulClassList(current);
|
|
90
|
+
if (classes.length > 0) {
|
|
91
|
+
segment = `${tag}.${classes[0]}`;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
parts.unshift(segment);
|
|
95
|
+
current = getParentElementCrossShadow(current);
|
|
96
|
+
depth += 1;
|
|
97
|
+
}
|
|
98
|
+
return parts.join(" > ");
|
|
99
|
+
}
|
|
100
|
+
export function getStableSelector(target) {
|
|
101
|
+
for (const attr of TEST_ID_ATTRS) {
|
|
102
|
+
const value = target.getAttribute(attr);
|
|
103
|
+
if (!value)
|
|
104
|
+
continue;
|
|
105
|
+
const raw = `[${attr}="${escapeCssValue(value)}"]`;
|
|
106
|
+
if (isUniqueSelector(raw))
|
|
107
|
+
return raw;
|
|
108
|
+
const tagged = `${target.tagName.toLowerCase()}${raw}`;
|
|
109
|
+
if (isUniqueSelector(tagged))
|
|
110
|
+
return tagged;
|
|
111
|
+
}
|
|
112
|
+
if (target.id) {
|
|
113
|
+
const idSelector = `#${escapeCssValue(target.id)}`;
|
|
114
|
+
if (isUniqueSelector(idSelector))
|
|
115
|
+
return idSelector;
|
|
116
|
+
}
|
|
117
|
+
const tag = target.tagName.toLowerCase();
|
|
118
|
+
const classes = getMeaningfulClassList(target);
|
|
119
|
+
for (let i = 0; i < Math.min(classes.length, 3); i += 1) {
|
|
120
|
+
const classSelector = `${tag}.${classes.slice(0, i + 1).join(".")}`;
|
|
121
|
+
if (isUniqueSelector(classSelector))
|
|
122
|
+
return classSelector;
|
|
123
|
+
}
|
|
124
|
+
const chain = [];
|
|
125
|
+
let current = target;
|
|
126
|
+
let guard = 0;
|
|
127
|
+
while (current && current.tagName.toLowerCase() !== "html" && guard < 8) {
|
|
128
|
+
const currentTag = current.tagName.toLowerCase();
|
|
129
|
+
const currentClasses = getMeaningfulClassList(current);
|
|
130
|
+
let segment = currentTag;
|
|
131
|
+
if (current.id) {
|
|
132
|
+
const idSelector = `#${escapeCssValue(current.id)}`;
|
|
133
|
+
chain.unshift(idSelector);
|
|
134
|
+
const joined = chain.join(" > ");
|
|
135
|
+
if (isUniqueSelector(joined))
|
|
136
|
+
return joined;
|
|
137
|
+
current = getParentElementCrossShadow(current);
|
|
138
|
+
guard += 1;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (currentClasses.length > 0) {
|
|
142
|
+
segment = `${currentTag}.${currentClasses[0]}`;
|
|
143
|
+
}
|
|
144
|
+
const index = getNthOfTypeIndex(current);
|
|
145
|
+
segment = `${segment}:nth-of-type(${index})`;
|
|
146
|
+
chain.unshift(segment);
|
|
147
|
+
const candidate = chain.join(" > ");
|
|
148
|
+
if (isUniqueSelector(candidate))
|
|
149
|
+
return candidate;
|
|
150
|
+
current = getParentElementCrossShadow(current);
|
|
151
|
+
guard += 1;
|
|
152
|
+
}
|
|
153
|
+
return `${tag}:nth-of-type(${getNthOfTypeIndex(target)})`;
|
|
154
|
+
}
|
|
155
|
+
export function identifyElementName(target) {
|
|
156
|
+
if (target.dataset.ekairosLabel)
|
|
157
|
+
return target.dataset.ekairosLabel;
|
|
158
|
+
const tag = target.tagName.toLowerCase();
|
|
159
|
+
const text = target.textContent?.trim() ?? "";
|
|
160
|
+
const shortText = text.slice(0, 40);
|
|
161
|
+
if (tag === "button") {
|
|
162
|
+
const ariaLabel = target.getAttribute("aria-label");
|
|
163
|
+
if (ariaLabel)
|
|
164
|
+
return `button [${ariaLabel}]`;
|
|
165
|
+
if (shortText)
|
|
166
|
+
return `button "${shortText}"`;
|
|
167
|
+
return "button";
|
|
168
|
+
}
|
|
169
|
+
if (tag === "a") {
|
|
170
|
+
if (shortText)
|
|
171
|
+
return `link "${shortText}"`;
|
|
172
|
+
const href = target.getAttribute("href");
|
|
173
|
+
if (href)
|
|
174
|
+
return `link (${href.slice(0, 60)})`;
|
|
175
|
+
return "link";
|
|
176
|
+
}
|
|
177
|
+
if (tag === "input" || tag === "textarea" || tag === "select") {
|
|
178
|
+
const name = target.getAttribute("name");
|
|
179
|
+
const placeholder = target.getAttribute("placeholder");
|
|
180
|
+
if (placeholder)
|
|
181
|
+
return `${tag} "${placeholder.slice(0, 30)}"`;
|
|
182
|
+
if (name)
|
|
183
|
+
return `${tag} [${name}]`;
|
|
184
|
+
return tag;
|
|
185
|
+
}
|
|
186
|
+
if (/^h[1-6]$/.test(tag)) {
|
|
187
|
+
if (shortText)
|
|
188
|
+
return `${tag} "${shortText}"`;
|
|
189
|
+
return tag;
|
|
190
|
+
}
|
|
191
|
+
if (shortText && shortText.length <= 30)
|
|
192
|
+
return `"${shortText}"`;
|
|
193
|
+
return tag;
|
|
194
|
+
}
|
|
195
|
+
export function getElementClasses(target) {
|
|
196
|
+
const classes = Array.from(target.classList)
|
|
197
|
+
.map((value) => value.trim())
|
|
198
|
+
.filter(Boolean);
|
|
199
|
+
return classes.join(", ");
|
|
200
|
+
}
|
|
201
|
+
export function getNearbyText(target) {
|
|
202
|
+
const pieces = [];
|
|
203
|
+
const currentText = target.textContent?.trim();
|
|
204
|
+
if (currentText && currentText.length <= 120) {
|
|
205
|
+
pieces.push(currentText);
|
|
206
|
+
}
|
|
207
|
+
const prevText = target.previousElementSibling?.textContent?.trim();
|
|
208
|
+
if (prevText && prevText.length <= 60) {
|
|
209
|
+
pieces.unshift(`[before "${prevText.slice(0, 50)}"]`);
|
|
210
|
+
}
|
|
211
|
+
const nextText = target.nextElementSibling?.textContent?.trim();
|
|
212
|
+
if (nextText && nextText.length <= 60) {
|
|
213
|
+
pieces.push(`[after "${nextText.slice(0, 50)}"]`);
|
|
214
|
+
}
|
|
215
|
+
return pieces.join(" ");
|
|
216
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import type { EkairosToolbarProps } from "./types";
|
|
2
|
+
export declare function EkairosToolbar({ onAnnotationAdd, onAnnotationDelete, onAnnotationUpdate, onAnnotationsClear, onCopy, onSubmit, copyToClipboard, blockInteractions, initialActive, outputDetail, storageKey, }?: EkairosToolbarProps): import("react").ReactPortal | null;
|