vite-plugin-source-locator 1.1.3 → 1.2.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 +9 -25
- package/dist/client/controller.d.ts +1 -2
- package/dist/client/controller.js +26 -46
- package/dist/client/index.js +2 -0
- package/dist/client/overlay-styles.d.ts +11 -4
- package/dist/client/overlay-styles.js +11 -4
- package/dist/client/overlay.d.ts +4 -4
- package/dist/client/overlay.js +16 -15
- package/dist/client/tooltip-text.d.ts +1 -2
- package/dist/client/tooltip-text.js +8 -22
- package/dist/shared/index.d.ts +0 -3
- package/dist/shared/index.js +0 -14
- package/dist/shared/theme.d.ts +1 -0
- package/dist/shared/theme.js +19 -7
- package/dist/vite/editor-cli.d.ts +1 -0
- package/dist/vite/editor-cli.js +9 -2
- package/dist/vite/editors.js +5 -5
- package/dist/vite/index.js +25 -3
- package/package.json +2 -2
- package/dist/client/css-source.d.ts +0 -1
- package/dist/client/css-source.js +0 -60
- package/dist/client/preference.d.ts +0 -2
- package/dist/client/preference.js +0 -7
- package/dist/client/source-panel.d.ts +0 -11
- package/dist/client/source-panel.js +0 -113
package/README.md
CHANGED
|
@@ -42,11 +42,15 @@ No `main.tsx` wiring required. The plugin auto-injects the client overlay in dev
|
|
|
42
42
|
|
|
43
43
|
## Pick Mode
|
|
44
44
|
|
|
45
|
-
1. Click the badge (bottom-right): **
|
|
45
|
+
1. Click the badge (bottom-right): **Locator**
|
|
46
46
|
2. Hover elements — blue highlight + file paths in tooltip
|
|
47
|
-
3. Click to open
|
|
47
|
+
3. Click to open the TSX source file in your IDE
|
|
48
48
|
4. **Esc** — cancel pick mode
|
|
49
|
-
|
|
49
|
+
|
|
50
|
+
| Shortcut | Action |
|
|
51
|
+
|----------|--------|
|
|
52
|
+
| Click | Open TSX source |
|
|
53
|
+
| Esc | Cancel pick |
|
|
50
54
|
|
|
51
55
|
## Exports
|
|
52
56
|
|
|
@@ -75,7 +79,7 @@ Control overlay colors (badge, tooltip, highlight). Presets or custom colors:
|
|
|
75
79
|
| Preset | Look |
|
|
76
80
|
|--------|------|
|
|
77
81
|
| `'default'` | Dark slate + cyan accent |
|
|
78
|
-
| `'light'` | White background + blue accent |
|
|
82
|
+
| `'light'` | White background + blue accent (also used when `theme` is omitted) |
|
|
79
83
|
| `'dark'` | Black background + white/gray text |
|
|
80
84
|
| `'blue'` | Navy background + light blue accent |
|
|
81
85
|
|
|
@@ -89,7 +93,6 @@ import { initSourceLocator } from 'vite-plugin-source-locator/client'
|
|
|
89
93
|
initSourceLocator({
|
|
90
94
|
endpoint: '/__open-in-editor',
|
|
91
95
|
attribute: 'data-source',
|
|
92
|
-
ides: ['auto', 'cursor', 'vscode', 'webstorm'],
|
|
93
96
|
theme: {
|
|
94
97
|
background: '#ffffff',
|
|
95
98
|
text: '#000000',
|
|
@@ -104,20 +107,6 @@ initSourceLocator({
|
|
|
104
107
|
| `text` | Tooltip text |
|
|
105
108
|
| `accent` | Borders, highlight, badge label |
|
|
106
109
|
|
|
107
|
-
## CSS Detection
|
|
108
|
-
|
|
109
|
-
When an element has styles from a project `.css` file (e.g. `index.css` custom classes like `.glass`), the tooltip shows:
|
|
110
|
-
|
|
111
|
-
```
|
|
112
|
-
components/DashboardStats.tsx
|
|
113
|
-
CSS: index.css
|
|
114
|
-
Click → TSX | next: CSS
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
Click cycles between opening the TSX and CSS file.
|
|
118
|
-
|
|
119
|
-
**Limitation:** Tailwind utility classes (`bg-slate-950`, etc.) are injected as inline `<style>` tags with no `.css` href — no CSS file is shown for those. CSS detection works for external `.css` stylesheets only. CSS opens at line 1 in v1.
|
|
120
|
-
|
|
121
110
|
## MFE Safety
|
|
122
111
|
|
|
123
112
|
- **Idempotent** — `window.__sourceLocator` guard: multiple bundles loading the locator mount only one overlay
|
|
@@ -132,7 +121,6 @@ import { initSourceLocator } from 'vite-plugin-source-locator/client'
|
|
|
132
121
|
initSourceLocator({
|
|
133
122
|
endpoint: '/__open-in-editor',
|
|
134
123
|
attribute: 'data-source',
|
|
135
|
-
ides: ['auto', 'cursor', 'vscode', 'webstorm'],
|
|
136
124
|
theme: 'blue',
|
|
137
125
|
})
|
|
138
126
|
```
|
|
@@ -157,12 +145,9 @@ sourceLocator()
|
|
|
157
145
|
|
|
158
146
|
// VS Code only
|
|
159
147
|
sourceLocator({ ides: ['vscode'] })
|
|
160
|
-
|
|
161
|
-
// auto + manual override with Shift+L (session only)
|
|
162
|
-
sourceLocator({ ides: ['auto', 'vscode'] })
|
|
163
148
|
```
|
|
164
149
|
|
|
165
|
-
The
|
|
150
|
+
The `ides` option controls which editors the server may open. The browser client always sends `ide=auto`; the server resolves that via `resolveIde` against your `ides` list.
|
|
166
151
|
|
|
167
152
|
### Explicit editor
|
|
168
153
|
|
|
@@ -190,7 +175,6 @@ REACT_EDITOR=/Applications/Visual Studio Code.app/Contents/Resources/app/bin/cod
|
|
|
190
175
|
|
|
191
176
|
- Dev only — no production impact
|
|
192
177
|
- JSX/TSX only for `data-source` injection
|
|
193
|
-
- CSS line 1 only (no source-map line mapping yet)
|
|
194
178
|
|
|
195
179
|
## License
|
|
196
180
|
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { LocatorThemeInput } from '../shared/index.js';
|
|
2
2
|
export type ClientConfig = {
|
|
3
3
|
endpoint: string;
|
|
4
4
|
attribute: string;
|
|
5
|
-
ides: LocatorIde[];
|
|
6
5
|
theme?: LocatorThemeInput;
|
|
7
6
|
};
|
|
8
7
|
export declare function startPickController(root: ShadowRoot, host: Element, config: ClientConfig): () => void;
|
|
@@ -1,49 +1,42 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { findCssSource } from './css-source.js';
|
|
1
|
+
import { DEFAULT_IDE, parseSourceLocation, resolveTheme } from '../shared/index.js';
|
|
3
2
|
import { createLocatorOverlayUi } from './overlay.js';
|
|
4
|
-
const EMPTY_SOURCES = Object.freeze({
|
|
5
|
-
tsx: undefined,
|
|
6
|
-
css: undefined,
|
|
7
|
-
clickTarget: 'tsx',
|
|
8
|
-
});
|
|
9
3
|
function getSourceEl(target, attribute, host) {
|
|
10
4
|
if (!target || host.contains(target) || target === host)
|
|
11
5
|
return null;
|
|
12
6
|
const el = target.closest(`[${attribute}]`);
|
|
13
7
|
return el instanceof HTMLElement ? el : null;
|
|
14
8
|
}
|
|
15
|
-
async function openSourceInEditor(source, config
|
|
9
|
+
async function openSourceInEditor(source, config) {
|
|
16
10
|
const loc = parseSourceLocation(source);
|
|
11
|
+
// Client always sends ide=auto; server resolveIde + plugin ides config pick the editor.
|
|
17
12
|
const params = new URLSearchParams({
|
|
18
13
|
file: loc.file,
|
|
19
14
|
line: loc.line,
|
|
20
15
|
col: loc.col,
|
|
21
|
-
ide:
|
|
16
|
+
ide: DEFAULT_IDE,
|
|
22
17
|
});
|
|
23
18
|
await fetch(`${config.endpoint}?${params.toString()}`);
|
|
24
19
|
}
|
|
25
|
-
function
|
|
26
|
-
|
|
27
|
-
const css = findCssSource(el);
|
|
28
|
-
return { tsx, css, clickTarget: 'tsx' };
|
|
29
|
-
}
|
|
30
|
-
function resolveOpenSource(sources) {
|
|
31
|
-
return sources[sources.clickTarget];
|
|
20
|
+
function readComponentSource(el, attribute) {
|
|
21
|
+
return el.getAttribute(attribute) ?? undefined;
|
|
32
22
|
}
|
|
33
23
|
export function startPickController(root, host, config) {
|
|
34
24
|
let pickMode = false;
|
|
35
|
-
let
|
|
36
|
-
|
|
37
|
-
const ui = createLocatorOverlayUi(root, () => setPickMode(!pickMode), resolveTheme(config.theme), () => activeIde);
|
|
25
|
+
let componentSource;
|
|
26
|
+
const ui = createLocatorOverlayUi(root, () => setPickMode(!pickMode), resolveTheme(config.theme));
|
|
38
27
|
function setPickMode(active) {
|
|
28
|
+
if (active)
|
|
29
|
+
document.addEventListener('mousemove', onMouseMove);
|
|
30
|
+
else
|
|
31
|
+
document.removeEventListener('mousemove', onMouseMove);
|
|
39
32
|
pickMode = active;
|
|
40
33
|
ui.setPickActive(active);
|
|
41
34
|
if (!active)
|
|
42
|
-
|
|
35
|
+
componentSource = undefined;
|
|
43
36
|
}
|
|
44
|
-
const
|
|
37
|
+
const syncSource = (el) => {
|
|
45
38
|
if (el !== ui.getActiveEl())
|
|
46
|
-
|
|
39
|
+
componentSource = readComponentSource(el, config.attribute);
|
|
47
40
|
};
|
|
48
41
|
const updateHover = (target, x, y) => {
|
|
49
42
|
const el = getSourceEl(target, config.attribute, host);
|
|
@@ -51,27 +44,15 @@ export function startPickController(root, host, config) {
|
|
|
51
44
|
ui.removeTooltip();
|
|
52
45
|
return;
|
|
53
46
|
}
|
|
54
|
-
|
|
55
|
-
ui.showSourceTooltip(el,
|
|
47
|
+
syncSource(el);
|
|
48
|
+
ui.showSourceTooltip(el, componentSource, x, y);
|
|
56
49
|
};
|
|
57
50
|
const onMouseMove = (e) => {
|
|
58
|
-
if (!pickMode) {
|
|
59
|
-
ui.removeTooltip();
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
51
|
updateHover(e.target, e.clientX, e.clientY);
|
|
63
52
|
};
|
|
64
53
|
const onKeyDown = (e) => {
|
|
65
|
-
if (e.key === 'Escape' && pickMode)
|
|
54
|
+
if (e.key === 'Escape' && pickMode)
|
|
66
55
|
setPickMode(false);
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
if (!e.shiftKey || e.key !== 'L')
|
|
70
|
-
return;
|
|
71
|
-
activeIde = nextIde(activeIde, config.ides);
|
|
72
|
-
if (!pickMode)
|
|
73
|
-
ui.refreshBadgeLabel();
|
|
74
|
-
ui.flashMessage(`IDE: ${activeIde}`);
|
|
75
56
|
};
|
|
76
57
|
const onClick = async (e) => {
|
|
77
58
|
if (!pickMode || host.contains(e.target))
|
|
@@ -79,24 +60,23 @@ export function startPickController(root, host, config) {
|
|
|
79
60
|
e.preventDefault();
|
|
80
61
|
e.stopPropagation();
|
|
81
62
|
const el = getSourceEl(e.target, config.attribute, host);
|
|
82
|
-
if (el)
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
if (!
|
|
63
|
+
if (!el)
|
|
64
|
+
return;
|
|
65
|
+
syncSource(el);
|
|
66
|
+
if (!componentSource) {
|
|
86
67
|
ui.flashMessage('No source for this element');
|
|
87
68
|
return;
|
|
88
69
|
}
|
|
89
|
-
await openSourceInEditor(
|
|
90
|
-
|
|
91
|
-
ui.showSourceTooltip(el, sources.tsx, sources.css, sources.clickTarget, e.clientX, e.clientY);
|
|
70
|
+
await openSourceInEditor(componentSource, config);
|
|
71
|
+
ui.showSourceTooltip(el, componentSource, e.clientX, e.clientY);
|
|
92
72
|
};
|
|
93
73
|
document.addEventListener('keydown', onKeyDown);
|
|
94
|
-
document.addEventListener('mousemove', onMouseMove);
|
|
95
74
|
document.addEventListener('click', onClick, true);
|
|
96
75
|
ui.mountBadge();
|
|
97
76
|
return () => {
|
|
77
|
+
setPickMode(false);
|
|
98
78
|
document.removeEventListener('keydown', onKeyDown);
|
|
99
|
-
document.removeEventListener('mousemove', onMouseMove);
|
|
100
79
|
document.removeEventListener('click', onClick, true);
|
|
80
|
+
ui.dispose();
|
|
101
81
|
};
|
|
102
82
|
}
|
package/dist/client/index.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { startPickController } from './controller.js';
|
|
2
|
+
import { HOST_LAYOUT } from './overlay-styles.js';
|
|
2
3
|
const HOST_ID = 'source-locator-host';
|
|
3
4
|
export function initSourceLocator(config) {
|
|
4
5
|
if (window.__sourceLocator || document.getElementById(HOST_ID))
|
|
5
6
|
return;
|
|
6
7
|
const host = document.createElement('div');
|
|
7
8
|
host.id = HOST_ID;
|
|
9
|
+
Object.assign(host.style, HOST_LAYOUT);
|
|
8
10
|
const root = host.attachShadow({ mode: 'open' });
|
|
9
11
|
document.body.appendChild(host);
|
|
10
12
|
const disposeController = startPickController(root, host, config);
|
|
@@ -3,18 +3,25 @@ export declare const UI_IDS: {
|
|
|
3
3
|
readonly tooltip: "source-locator-tooltip";
|
|
4
4
|
readonly highlight: "source-locator-highlight";
|
|
5
5
|
};
|
|
6
|
+
export declare const HOST_LAYOUT: {
|
|
7
|
+
readonly position: "fixed";
|
|
8
|
+
readonly inset: "0";
|
|
9
|
+
readonly zIndex: "99999";
|
|
10
|
+
readonly pointerEvents: "none";
|
|
11
|
+
};
|
|
6
12
|
export declare const LAYOUT: {
|
|
7
13
|
readonly badge: {
|
|
8
14
|
readonly position: "fixed";
|
|
9
15
|
readonly bottom: "12px";
|
|
10
16
|
readonly right: "12px";
|
|
11
|
-
readonly padding: "
|
|
12
|
-
readonly borderRadius: "
|
|
13
|
-
readonly fontSize: "
|
|
17
|
+
readonly padding: "10px 14px";
|
|
18
|
+
readonly borderRadius: "8px";
|
|
19
|
+
readonly fontSize: "12px";
|
|
14
20
|
readonly fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace";
|
|
15
21
|
readonly zIndex: "99999";
|
|
16
22
|
readonly cursor: "pointer";
|
|
17
|
-
readonly
|
|
23
|
+
readonly pointerEvents: "auto";
|
|
24
|
+
readonly boxShadow: "0 2px 12px rgba(0, 0, 0, 0.12), 0 0 0 1px rgba(0, 0, 0, 0.08)";
|
|
18
25
|
};
|
|
19
26
|
readonly tooltip: {
|
|
20
27
|
readonly position: "fixed";
|
|
@@ -3,18 +3,25 @@ export const UI_IDS = {
|
|
|
3
3
|
tooltip: 'source-locator-tooltip',
|
|
4
4
|
highlight: 'source-locator-highlight',
|
|
5
5
|
};
|
|
6
|
+
export const HOST_LAYOUT = {
|
|
7
|
+
position: 'fixed',
|
|
8
|
+
inset: '0',
|
|
9
|
+
zIndex: '99999',
|
|
10
|
+
pointerEvents: 'none',
|
|
11
|
+
};
|
|
6
12
|
export const LAYOUT = {
|
|
7
13
|
badge: {
|
|
8
14
|
position: 'fixed',
|
|
9
15
|
bottom: '12px',
|
|
10
16
|
right: '12px',
|
|
11
|
-
padding: '
|
|
12
|
-
borderRadius: '
|
|
13
|
-
fontSize: '
|
|
17
|
+
padding: '10px 14px',
|
|
18
|
+
borderRadius: '8px',
|
|
19
|
+
fontSize: '12px',
|
|
14
20
|
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace',
|
|
15
21
|
zIndex: '99999',
|
|
16
22
|
cursor: 'pointer',
|
|
17
|
-
|
|
23
|
+
pointerEvents: 'auto',
|
|
24
|
+
boxShadow: '0 2px 12px rgba(0, 0, 0, 0.12), 0 0 0 1px rgba(0, 0, 0, 0.08)',
|
|
18
25
|
},
|
|
19
26
|
tooltip: {
|
|
20
27
|
position: 'fixed',
|
package/dist/client/overlay.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
export declare function createLocatorOverlayUi(root: ShadowRoot, onTogglePick: () => void, theme: LocatorTheme
|
|
1
|
+
import type { LocatorTheme } from '../shared/index.js';
|
|
2
|
+
export declare function createLocatorOverlayUi(root: ShadowRoot, onTogglePick: () => void, theme: LocatorTheme): {
|
|
3
3
|
mountBadge: () => void;
|
|
4
4
|
setPickActive: (active: boolean) => void;
|
|
5
|
-
|
|
6
|
-
showSourceTooltip: (el: Element, tsxSource: string | undefined, cssSource: string | undefined, clickTarget: ClickTarget, x: number, y: number) => void;
|
|
5
|
+
showSourceTooltip: (el: Element, source: string | undefined, x: number, y: number) => void;
|
|
7
6
|
flashMessage: (text: string) => void;
|
|
8
7
|
removeTooltip: () => void;
|
|
8
|
+
dispose: () => void;
|
|
9
9
|
getActiveEl: () => Element | null;
|
|
10
10
|
};
|
|
11
11
|
export type LocatorOverlayUi = ReturnType<typeof createLocatorOverlayUi>;
|
package/dist/client/overlay.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { LAYOUT, UI_IDS } from './overlay-styles.js';
|
|
2
|
-
import { badgeLabel } from './preference.js';
|
|
3
2
|
import { buildTooltipText } from './tooltip-text.js';
|
|
3
|
+
const BADGE_LABEL_IDLE = 'Locator';
|
|
4
|
+
const BADGE_LABEL_PICKING = 'Picking — Esc';
|
|
4
5
|
const HIGHLIGHT_PADDING = 2;
|
|
5
6
|
const TOOLTIP_CURSOR_OFFSET = 16;
|
|
6
7
|
const FLASH_DURATION_MS = 1500;
|
|
7
8
|
const FLASH_HORIZONTAL_OFFSET = 80;
|
|
8
9
|
const FLASH_BOTTOM_OFFSET = 80;
|
|
9
|
-
export function createLocatorOverlayUi(root, onTogglePick, theme
|
|
10
|
+
export function createLocatorOverlayUi(root, onTogglePick, theme) {
|
|
10
11
|
let activeEl = null;
|
|
11
12
|
let flashTimeout = null;
|
|
12
13
|
let badgeEl = null;
|
|
@@ -49,8 +50,8 @@ export function createLocatorOverlayUi(root, onTogglePick, theme, getActiveIde)
|
|
|
49
50
|
if (el)
|
|
50
51
|
showHighlight(el);
|
|
51
52
|
};
|
|
52
|
-
const showSourceTooltip = (el,
|
|
53
|
-
showTooltip(buildTooltipText(
|
|
53
|
+
const showSourceTooltip = (el, source, x, y) => {
|
|
54
|
+
showTooltip(buildTooltipText(source), el, x, y);
|
|
54
55
|
};
|
|
55
56
|
const flashMessage = (text) => {
|
|
56
57
|
if (flashTimeout)
|
|
@@ -65,43 +66,43 @@ export function createLocatorOverlayUi(root, onTogglePick, theme, getActiveIde)
|
|
|
65
66
|
return;
|
|
66
67
|
badgeEl.style.background = active ? theme.badgeActiveBackground : theme.badgeBackground;
|
|
67
68
|
badgeEl.style.color = active ? theme.badgeActiveText : theme.badgeText;
|
|
69
|
+
badgeEl.style.border = `1px solid ${active ? theme.badgeActiveBorder : theme.badgeBorder}`;
|
|
68
70
|
};
|
|
69
71
|
const setPickActive = (active) => {
|
|
70
72
|
document.body.style.cursor = active ? 'crosshair' : '';
|
|
71
73
|
if (!badgeEl)
|
|
72
74
|
return;
|
|
73
|
-
badgeEl.textContent =
|
|
75
|
+
badgeEl.textContent = active ? BADGE_LABEL_PICKING : BADGE_LABEL_IDLE;
|
|
74
76
|
applyBadgeColors(active);
|
|
75
77
|
if (!active)
|
|
76
78
|
removeTooltip();
|
|
77
79
|
};
|
|
78
|
-
const
|
|
79
|
-
if (
|
|
80
|
-
|
|
80
|
+
const dispose = () => {
|
|
81
|
+
if (flashTimeout)
|
|
82
|
+
clearTimeout(flashTimeout);
|
|
83
|
+
flashTimeout = null;
|
|
84
|
+
removeTooltip();
|
|
81
85
|
};
|
|
82
86
|
const mountBadge = () => {
|
|
83
87
|
badgeEl = document.createElement('button');
|
|
84
88
|
badgeEl.id = UI_IDS.badge;
|
|
85
89
|
badgeEl.type = 'button';
|
|
86
|
-
badgeEl.textContent =
|
|
87
|
-
Object.assign(badgeEl.style, LAYOUT.badge
|
|
88
|
-
background: theme.badgeBackground,
|
|
89
|
-
color: theme.badgeText,
|
|
90
|
-
border: `1px solid ${theme.badgeBorder}`,
|
|
91
|
-
});
|
|
90
|
+
badgeEl.textContent = BADGE_LABEL_IDLE;
|
|
91
|
+
Object.assign(badgeEl.style, LAYOUT.badge);
|
|
92
92
|
badgeEl.addEventListener('click', (event) => {
|
|
93
93
|
event.stopPropagation();
|
|
94
94
|
onTogglePick();
|
|
95
95
|
});
|
|
96
96
|
root.appendChild(badgeEl);
|
|
97
|
+
applyBadgeColors(false);
|
|
97
98
|
};
|
|
98
99
|
return {
|
|
99
100
|
mountBadge,
|
|
100
101
|
setPickActive,
|
|
101
|
-
refreshBadgeLabel,
|
|
102
102
|
showSourceTooltip,
|
|
103
103
|
flashMessage,
|
|
104
104
|
removeTooltip,
|
|
105
|
+
dispose,
|
|
105
106
|
getActiveEl: () => activeEl,
|
|
106
107
|
};
|
|
107
108
|
}
|
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export declare function buildTooltipText(tsxSource: string | undefined, cssSource: string | undefined, clickTarget: ClickTarget): string;
|
|
1
|
+
export declare function buildTooltipText(source: string | undefined): string;
|
|
@@ -1,28 +1,14 @@
|
|
|
1
1
|
import { parseSourceLocation } from '../shared/index.js';
|
|
2
2
|
const DEFAULT_LINE = '1';
|
|
3
|
-
|
|
4
|
-
tsx: ['TSX', 'CSS'],
|
|
5
|
-
css: ['CSS', 'TSX'],
|
|
6
|
-
};
|
|
7
|
-
function formatSourceLabel(source, prefix) {
|
|
3
|
+
function formatSourceLabel(source) {
|
|
8
4
|
const { file, line } = parseSourceLocation(source);
|
|
9
5
|
const name = file.split('/').pop() ?? file;
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
return label;
|
|
6
|
+
if (line !== DEFAULT_LINE)
|
|
7
|
+
return `${name}:${line}`;
|
|
8
|
+
return name;
|
|
14
9
|
}
|
|
15
|
-
export function buildTooltipText(
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
if (cssSource)
|
|
20
|
-
lines.push(formatSourceLabel(cssSource, 'CSS'));
|
|
21
|
-
if (!cssSource) {
|
|
22
|
-
lines.push('Click → open TSX');
|
|
23
|
-
return lines.join('\n');
|
|
24
|
-
}
|
|
25
|
-
const [first, second] = CLICK_PROMPT_ORDER[clickTarget];
|
|
26
|
-
lines.push(`Click → open ${first}`, `Click again → open ${second}`);
|
|
27
|
-
return lines.join('\n');
|
|
10
|
+
export function buildTooltipText(source) {
|
|
11
|
+
if (!source)
|
|
12
|
+
return 'No source for this element';
|
|
13
|
+
return `${formatSourceLabel(source)}\nClick → open`;
|
|
28
14
|
}
|
package/dist/shared/index.d.ts
CHANGED
|
@@ -6,14 +6,11 @@ export type SourceLocation = {
|
|
|
6
6
|
line: string;
|
|
7
7
|
col: string;
|
|
8
8
|
};
|
|
9
|
-
export type ClickTarget = 'tsx' | 'css';
|
|
10
9
|
export declare const SOURCE_ATTR = "data-source";
|
|
11
10
|
export declare const OPEN_ENDPOINT = "/__open-in-editor";
|
|
12
11
|
export declare const DEFAULT_IDE: LocatorIde;
|
|
13
12
|
export declare const IDE_ORDER: LocatorIde[];
|
|
14
13
|
export declare function isLocatorIde(value: string): value is LocatorIde;
|
|
15
14
|
export declare function resolveIde(value: string, allowed: LocatorIde[]): LocatorIde;
|
|
16
|
-
export declare function nextIde(current: LocatorIde, order?: LocatorIde[]): LocatorIde;
|
|
17
15
|
export declare function parseSourceLocation(raw: string): SourceLocation;
|
|
18
16
|
export declare function formatSourceLocation(loc: SourceLocation): string;
|
|
19
|
-
export declare function nextClickTarget(current: ClickTarget, hasCss: boolean): ClickTarget;
|
package/dist/shared/index.js
CHANGED
|
@@ -14,11 +14,6 @@ export function resolveIde(value, allowed) {
|
|
|
14
14
|
return fallback;
|
|
15
15
|
return value;
|
|
16
16
|
}
|
|
17
|
-
export function nextIde(current, order = IDE_ORDER) {
|
|
18
|
-
const index = order.indexOf(current);
|
|
19
|
-
const nextIndex = (index + 1) % order.length;
|
|
20
|
-
return order[nextIndex] ?? DEFAULT_IDE;
|
|
21
|
-
}
|
|
22
17
|
export function parseSourceLocation(raw) {
|
|
23
18
|
const parts = raw.split(':');
|
|
24
19
|
if (parts.length < 3)
|
|
@@ -30,12 +25,3 @@ export function parseSourceLocation(raw) {
|
|
|
30
25
|
export function formatSourceLocation(loc) {
|
|
31
26
|
return `${loc.file}:${loc.line}:${loc.col}`;
|
|
32
27
|
}
|
|
33
|
-
const CLICK_TARGET_TRANSITION = {
|
|
34
|
-
tsx: 'css',
|
|
35
|
-
css: 'tsx',
|
|
36
|
-
};
|
|
37
|
-
export function nextClickTarget(current, hasCss) {
|
|
38
|
-
if (!hasCss)
|
|
39
|
-
return 'tsx';
|
|
40
|
-
return CLICK_TARGET_TRANSITION[current];
|
|
41
|
-
}
|
package/dist/shared/theme.d.ts
CHANGED
package/dist/shared/theme.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
const PRESET_COLORS = {
|
|
2
2
|
default: { background: '#0f172a', text: '#f8fafc', accent: '#38bdf8' },
|
|
3
|
-
light: { background: '#ffffff', text: '#
|
|
3
|
+
light: { background: '#ffffff', text: '#111827', accent: '#2563eb' },
|
|
4
4
|
dark: { background: '#000000', text: '#ffffff', accent: '#a3a3a3' },
|
|
5
5
|
blue: { background: '#1e3a8a', text: '#eff6ff', accent: '#60a5fa' },
|
|
6
6
|
};
|
|
7
|
+
const LIGHT_BADGE_BORDER = '#d1d5db';
|
|
7
8
|
const HEX_COLOR_LENGTH = 7;
|
|
8
9
|
const RGB_CHANNEL_MAX = 255;
|
|
9
10
|
const HIGHLIGHT_ALPHA = 0.12;
|
|
@@ -16,14 +17,15 @@ function withAlpha(hex, alpha) {
|
|
|
16
17
|
.padStart(2, '0');
|
|
17
18
|
return `${hex}${channel}`;
|
|
18
19
|
}
|
|
19
|
-
function buildTheme(colors) {
|
|
20
|
+
function buildTheme(colors, preset) {
|
|
20
21
|
const { background, text, accent } = colors;
|
|
21
|
-
|
|
22
|
+
const base = {
|
|
22
23
|
badgeBackground: background,
|
|
23
24
|
badgeText: accent,
|
|
24
25
|
badgeActiveBackground: accent,
|
|
25
26
|
badgeActiveText: background,
|
|
26
27
|
badgeBorder: accent,
|
|
28
|
+
badgeActiveBorder: accent,
|
|
27
29
|
tooltipBackground: background,
|
|
28
30
|
tooltipText: text,
|
|
29
31
|
tooltipBorder: accent,
|
|
@@ -31,12 +33,22 @@ function buildTheme(colors) {
|
|
|
31
33
|
highlightBackground: withAlpha(accent, HIGHLIGHT_ALPHA),
|
|
32
34
|
highlightShadow: withAlpha(background, SHADOW_ALPHA),
|
|
33
35
|
};
|
|
36
|
+
if (preset !== 'light')
|
|
37
|
+
return base;
|
|
38
|
+
return {
|
|
39
|
+
...base,
|
|
40
|
+
badgeText: text,
|
|
41
|
+
badgeActiveBackground: background,
|
|
42
|
+
badgeActiveText: accent,
|
|
43
|
+
badgeBorder: LIGHT_BADGE_BORDER,
|
|
44
|
+
tooltipBorder: LIGHT_BADGE_BORDER,
|
|
45
|
+
};
|
|
34
46
|
}
|
|
35
47
|
export function resolveTheme(input) {
|
|
36
|
-
const
|
|
48
|
+
const lightBase = PRESET_COLORS.light;
|
|
37
49
|
if (!input)
|
|
38
|
-
return buildTheme(
|
|
50
|
+
return buildTheme(lightBase, 'light');
|
|
39
51
|
if (typeof input === 'string')
|
|
40
|
-
return buildTheme(PRESET_COLORS[input] ??
|
|
41
|
-
return buildTheme({ ...
|
|
52
|
+
return buildTheme(PRESET_COLORS[input] ?? lightBase, input);
|
|
53
|
+
return buildTheme({ ...lightBase, ...input }, 'light');
|
|
42
54
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { LocatorIde } from '../shared/index.js';
|
|
2
2
|
export declare function toLaunchEditorName(ide: Exclude<LocatorIde, 'auto'>): string;
|
|
3
3
|
export declare function resolveCliPath(command: string): string;
|
|
4
|
+
export declare function formatLaunchEditorCommand(command: string): string;
|
|
4
5
|
export declare function resolveAutoEditor(): string | null;
|
package/dist/vite/editor-cli.js
CHANGED
|
@@ -26,19 +26,26 @@ function buildCliCandidates() {
|
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
28
|
const CLI_CANDIDATES = buildCliCandidates();
|
|
29
|
+
const isPathLike = (value) => value.includes('/') || value.includes('\\');
|
|
29
30
|
export function toLaunchEditorName(ide) {
|
|
30
31
|
return IDE_LAUNCH_NAMES[ide];
|
|
31
32
|
}
|
|
32
33
|
export function resolveCliPath(command) {
|
|
33
|
-
if ((command
|
|
34
|
+
if (isPathLike(command) && existsSync(command)) {
|
|
34
35
|
return command;
|
|
35
36
|
}
|
|
36
37
|
const candidates = CLI_CANDIDATES[command] ?? [command];
|
|
37
|
-
const bundlePath = candidates.find((path) => (path
|
|
38
|
+
const bundlePath = candidates.find((path) => isPathLike(path) && existsSync(path));
|
|
38
39
|
if (bundlePath)
|
|
39
40
|
return bundlePath;
|
|
40
41
|
return command;
|
|
41
42
|
}
|
|
43
|
+
export function formatLaunchEditorCommand(command) {
|
|
44
|
+
if (command.includes(' ')) {
|
|
45
|
+
return `"${command}"`;
|
|
46
|
+
}
|
|
47
|
+
return command;
|
|
48
|
+
}
|
|
42
49
|
export function resolveAutoEditor() {
|
|
43
50
|
const [editor] = guessEditor();
|
|
44
51
|
if (!editor)
|
package/dist/vite/editors.js
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
import launch from 'launch-editor';
|
|
2
|
-
import { resolveIde } from '../shared/index.js';
|
|
3
|
-
import { resolveAutoEditor, resolveCliPath, toLaunchEditorName } from './editor-cli.js';
|
|
2
|
+
import { formatSourceLocation, resolveIde } from '../shared/index.js';
|
|
3
|
+
import { formatLaunchEditorCommand, resolveAutoEditor, resolveCliPath, toLaunchEditorName } from './editor-cli.js';
|
|
4
4
|
export function openInEditor(loc, ideParam, allowed) {
|
|
5
5
|
const ide = resolveIde(ideParam, allowed);
|
|
6
|
-
const spec =
|
|
6
|
+
const spec = formatSourceLocation(loc);
|
|
7
7
|
if (ide === 'auto') {
|
|
8
8
|
const resolved = resolveAutoEditor();
|
|
9
9
|
if (resolved) {
|
|
10
|
-
launch(spec, resolved);
|
|
10
|
+
launch(spec, formatLaunchEditorCommand(resolved));
|
|
11
11
|
return;
|
|
12
12
|
}
|
|
13
13
|
launch(spec);
|
|
14
14
|
return;
|
|
15
15
|
}
|
|
16
16
|
const command = resolveCliPath(toLaunchEditorName(ide));
|
|
17
|
-
launch(spec, command);
|
|
17
|
+
launch(spec, formatLaunchEditorCommand(command));
|
|
18
18
|
}
|
package/dist/vite/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
|
-
import { isAbsolute, resolve } from 'node:path';
|
|
2
|
+
import { isAbsolute, relative, resolve } from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import { IDE_ORDER, OPEN_ENDPOINT, SOURCE_ATTR } from '../shared/index.js';
|
|
5
5
|
import { babelPluginAddSourceAttr } from './babel-plugin.js';
|
|
@@ -25,12 +25,25 @@ function readQuery(url, defaultIde) {
|
|
|
25
25
|
ide: parsed.searchParams.get('ide') ?? defaultIde,
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
|
+
function isInsideRoot(resolved, root) {
|
|
29
|
+
const rel = relative(root, resolved);
|
|
30
|
+
return rel !== '' && !rel.startsWith('..') && !isAbsolute(rel);
|
|
31
|
+
}
|
|
32
|
+
function isAllowedRequest(req) {
|
|
33
|
+
const origin = req.headers.origin;
|
|
34
|
+
const host = req.headers.host;
|
|
35
|
+
if (origin && host && new URL(origin).host !== host)
|
|
36
|
+
return false;
|
|
37
|
+
if (req.headers['sec-fetch-site'] === 'cross-site')
|
|
38
|
+
return false;
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
28
41
|
function resolveFilePath(file, root) {
|
|
29
42
|
const viteDevMatch = file.match(/^\/src\/(.+)$/);
|
|
30
43
|
if (viteDevMatch)
|
|
31
44
|
return resolve(root, 'src', viteDevMatch[1]);
|
|
32
45
|
if (isAbsolute(file))
|
|
33
|
-
return file;
|
|
46
|
+
return resolve(file);
|
|
34
47
|
return resolve(root, file);
|
|
35
48
|
}
|
|
36
49
|
function sourceLocator(options = {}) {
|
|
@@ -38,7 +51,6 @@ function sourceLocator(options = {}) {
|
|
|
38
51
|
const clientConfig = {
|
|
39
52
|
endpoint: config.endpoint,
|
|
40
53
|
attribute: config.attribute,
|
|
41
|
-
ides: config.ides,
|
|
42
54
|
theme: config.theme,
|
|
43
55
|
};
|
|
44
56
|
return {
|
|
@@ -56,6 +68,11 @@ function sourceLocator(options = {}) {
|
|
|
56
68
|
},
|
|
57
69
|
configureServer(server) {
|
|
58
70
|
server.middlewares.use(config.endpoint, (req, res) => {
|
|
71
|
+
if (!isAllowedRequest(req)) {
|
|
72
|
+
res.writeHead(403);
|
|
73
|
+
res.end('forbidden');
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
59
76
|
const { file, line, col, ide } = readQuery(req.url ?? '', config.ides[0] ?? 'auto');
|
|
60
77
|
if (!file) {
|
|
61
78
|
res.writeHead(400);
|
|
@@ -64,6 +81,11 @@ function sourceLocator(options = {}) {
|
|
|
64
81
|
}
|
|
65
82
|
try {
|
|
66
83
|
const resolvedFile = resolveFilePath(file, server.config.root);
|
|
84
|
+
if (!isInsideRoot(resolvedFile, server.config.root)) {
|
|
85
|
+
res.writeHead(403);
|
|
86
|
+
res.end('outside project');
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
67
89
|
if (!existsSync(resolvedFile)) {
|
|
68
90
|
res.writeHead(404);
|
|
69
91
|
res.end('file not found');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vite-plugin-source-locator",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Dev-only Vite plugin: click UI elements to jump to source in your IDE",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"prepublishOnly": "npm run lint && npm run build && npm run test"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
|
-
"vite": "^5.0.0"
|
|
35
|
+
"vite": "^5.0.0 || ^6.0.0"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@babel/core": "^7.29.7",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function findCssSource(element: Element): string | undefined;
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import { formatSourceLocation } from '../shared/index.js';
|
|
2
|
-
const SELECTOR_ID_BONUS = 100;
|
|
3
|
-
const SELECTOR_CLASS_BONUS = 50;
|
|
4
|
-
const SELECTOR_CLASS_MATCH_BONUS = 200;
|
|
5
|
-
function scoreSelector(selector, element) {
|
|
6
|
-
let score = selector.length;
|
|
7
|
-
if (selector.includes('#'))
|
|
8
|
-
score += SELECTOR_ID_BONUS;
|
|
9
|
-
if (selector.includes('.'))
|
|
10
|
-
score += SELECTOR_CLASS_BONUS;
|
|
11
|
-
if (element instanceof HTMLElement) {
|
|
12
|
-
element.classList.forEach((cls) => {
|
|
13
|
-
if (selector.includes(`.${cls}`))
|
|
14
|
-
score += SELECTOR_CLASS_MATCH_BONUS;
|
|
15
|
-
});
|
|
16
|
-
}
|
|
17
|
-
return score;
|
|
18
|
-
}
|
|
19
|
-
function isProjectStylesheet(href) {
|
|
20
|
-
return href.includes('.css') && (href.includes('localhost') || href.includes('/src/'));
|
|
21
|
-
}
|
|
22
|
-
function hrefToFile(href) {
|
|
23
|
-
const url = new URL(href, window.location.origin);
|
|
24
|
-
return decodeURIComponent(url.pathname);
|
|
25
|
-
}
|
|
26
|
-
export function findCssSource(element) {
|
|
27
|
-
let bestScore = -1;
|
|
28
|
-
let bestFile;
|
|
29
|
-
Array.from(document.styleSheets).forEach((sheet) => {
|
|
30
|
-
const href = sheet.href ?? '';
|
|
31
|
-
if (!href || !isProjectStylesheet(href))
|
|
32
|
-
return;
|
|
33
|
-
let rules;
|
|
34
|
-
try {
|
|
35
|
-
rules = sheet.cssRules;
|
|
36
|
-
}
|
|
37
|
-
catch {
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
Array.from(rules).forEach((rule) => {
|
|
41
|
-
if (!(rule instanceof CSSStyleRule))
|
|
42
|
-
return;
|
|
43
|
-
try {
|
|
44
|
-
if (!element.matches(rule.selectorText))
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
catch {
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
const score = scoreSelector(rule.selectorText, element);
|
|
51
|
-
if (score <= bestScore)
|
|
52
|
-
return;
|
|
53
|
-
bestScore = score;
|
|
54
|
-
bestFile = hrefToFile(href);
|
|
55
|
-
});
|
|
56
|
-
});
|
|
57
|
-
if (!bestFile)
|
|
58
|
-
return undefined;
|
|
59
|
-
return formatSourceLocation({ file: bestFile, line: '1', col: '1' });
|
|
60
|
-
}
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
const BADGE_PICKING_HINT = 'Esc to cancel';
|
|
2
|
-
const BADGE_IDLE_HINT = 'click to pick';
|
|
3
|
-
export function badgeLabel(picking, activeIde) {
|
|
4
|
-
if (picking)
|
|
5
|
-
return `Pick element (${activeIde}) — ${BADGE_PICKING_HINT}`;
|
|
6
|
-
return `Source Locator (${activeIde}) — ${BADGE_IDLE_HINT}`;
|
|
7
|
-
}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import type { LocatorTheme } from '../shared/index.js';
|
|
2
|
-
export type SourcePanelPayload = {
|
|
3
|
-
file: string;
|
|
4
|
-
line: string;
|
|
5
|
-
col: string;
|
|
6
|
-
content: string;
|
|
7
|
-
};
|
|
8
|
-
export declare function createSourcePanel(root: ShadowRoot, theme: LocatorTheme): {
|
|
9
|
-
showSourcePanel: (payload: SourcePanelPayload) => void;
|
|
10
|
-
removePanel: () => void | undefined;
|
|
11
|
-
};
|
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
const PANEL_ID = 'source-locator-panel';
|
|
2
|
-
function fileBasename(file) {
|
|
3
|
-
return file.split('/').pop() ?? file;
|
|
4
|
-
}
|
|
5
|
-
function escapeHtml(text) {
|
|
6
|
-
return text
|
|
7
|
-
.replace(/&/g, '&')
|
|
8
|
-
.replace(/</g, '<')
|
|
9
|
-
.replace(/>/g, '>');
|
|
10
|
-
}
|
|
11
|
-
export function createSourcePanel(root, theme) {
|
|
12
|
-
const removePanel = () => root.getElementById(PANEL_ID)?.remove();
|
|
13
|
-
const showSourcePanel = (payload) => {
|
|
14
|
-
removePanel();
|
|
15
|
-
const lineNumber = Math.max(1, Number.parseInt(payload.line, 10) || 1);
|
|
16
|
-
const lines = payload.content.split('\n');
|
|
17
|
-
const panel = document.createElement('div');
|
|
18
|
-
panel.id = PANEL_ID;
|
|
19
|
-
Object.assign(panel.style, {
|
|
20
|
-
position: 'fixed',
|
|
21
|
-
left: '0',
|
|
22
|
-
right: '0',
|
|
23
|
-
bottom: '0',
|
|
24
|
-
height: '40vh',
|
|
25
|
-
minHeight: '200px',
|
|
26
|
-
maxHeight: '480px',
|
|
27
|
-
display: 'flex',
|
|
28
|
-
flexDirection: 'column',
|
|
29
|
-
background: theme.tooltipBackground,
|
|
30
|
-
borderTop: `2px solid ${theme.tooltipBorder}`,
|
|
31
|
-
boxShadow: '0 -8px 32px rgba(0, 0, 0, 0.45)',
|
|
32
|
-
zIndex: '100000',
|
|
33
|
-
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace',
|
|
34
|
-
fontSize: '12px',
|
|
35
|
-
});
|
|
36
|
-
const header = document.createElement('div');
|
|
37
|
-
Object.assign(header.style, {
|
|
38
|
-
display: 'flex',
|
|
39
|
-
alignItems: 'center',
|
|
40
|
-
justifyContent: 'space-between',
|
|
41
|
-
padding: '8px 12px',
|
|
42
|
-
borderBottom: `1px solid ${theme.tooltipBorder}`,
|
|
43
|
-
color: theme.tooltipText,
|
|
44
|
-
flexShrink: '0',
|
|
45
|
-
});
|
|
46
|
-
const title = document.createElement('span');
|
|
47
|
-
title.textContent = `${fileBasename(payload.file)}:${payload.line}:${payload.col}`;
|
|
48
|
-
const closeBtn = document.createElement('button');
|
|
49
|
-
closeBtn.type = 'button';
|
|
50
|
-
closeBtn.textContent = '×';
|
|
51
|
-
closeBtn.setAttribute('aria-label', 'Close source panel');
|
|
52
|
-
Object.assign(closeBtn.style, {
|
|
53
|
-
background: 'transparent',
|
|
54
|
-
border: 'none',
|
|
55
|
-
color: theme.badgeText,
|
|
56
|
-
fontSize: '20px',
|
|
57
|
-
lineHeight: '1',
|
|
58
|
-
cursor: 'pointer',
|
|
59
|
-
padding: '0 4px',
|
|
60
|
-
});
|
|
61
|
-
closeBtn.addEventListener('click', removePanel);
|
|
62
|
-
header.appendChild(title);
|
|
63
|
-
header.appendChild(closeBtn);
|
|
64
|
-
const body = document.createElement('div');
|
|
65
|
-
Object.assign(body.style, {
|
|
66
|
-
flex: '1',
|
|
67
|
-
overflow: 'auto',
|
|
68
|
-
margin: '0',
|
|
69
|
-
});
|
|
70
|
-
const pre = document.createElement('pre');
|
|
71
|
-
Object.assign(pre.style, {
|
|
72
|
-
margin: '0',
|
|
73
|
-
padding: '8px 0',
|
|
74
|
-
color: theme.tooltipText,
|
|
75
|
-
});
|
|
76
|
-
const highlightedLineId = 'source-locator-panel-highlight';
|
|
77
|
-
lines.forEach((lineText, index) => {
|
|
78
|
-
const lineEl = document.createElement('div');
|
|
79
|
-
const lineNum = index + 1;
|
|
80
|
-
const isTarget = lineNum === lineNumber;
|
|
81
|
-
Object.assign(lineEl.style, {
|
|
82
|
-
display: 'flex',
|
|
83
|
-
padding: '0 12px',
|
|
84
|
-
background: isTarget ? theme.highlightBackground : 'transparent',
|
|
85
|
-
borderLeft: isTarget ? `3px solid ${theme.highlightBorder}` : '3px solid transparent',
|
|
86
|
-
});
|
|
87
|
-
if (isTarget)
|
|
88
|
-
lineEl.id = highlightedLineId;
|
|
89
|
-
const num = document.createElement('span');
|
|
90
|
-
num.textContent = String(lineNum).padStart(4, ' ');
|
|
91
|
-
Object.assign(num.style, {
|
|
92
|
-
flexShrink: '0',
|
|
93
|
-
width: '48px',
|
|
94
|
-
color: theme.badgeText,
|
|
95
|
-
opacity: '0.7',
|
|
96
|
-
userSelect: 'none',
|
|
97
|
-
});
|
|
98
|
-
const code = document.createElement('span');
|
|
99
|
-
code.innerHTML = escapeHtml(lineText) || ' ';
|
|
100
|
-
lineEl.appendChild(num);
|
|
101
|
-
lineEl.appendChild(code);
|
|
102
|
-
pre.appendChild(lineEl);
|
|
103
|
-
});
|
|
104
|
-
body.appendChild(pre);
|
|
105
|
-
panel.appendChild(header);
|
|
106
|
-
panel.appendChild(body);
|
|
107
|
-
root.appendChild(panel);
|
|
108
|
-
requestAnimationFrame(() => {
|
|
109
|
-
root.getElementById(highlightedLineId)?.scrollIntoView({ block: 'center' });
|
|
110
|
-
});
|
|
111
|
-
};
|
|
112
|
-
return { showSourcePanel, removePanel };
|
|
113
|
-
}
|