vite-plugin-source-locator 1.1.4 → 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 CHANGED
@@ -44,13 +44,12 @@ No `main.tsx` wiring required. The plugin auto-injects the client overlay in dev
44
44
 
45
45
  1. Click the badge (bottom-right): **Locator**
46
46
  2. Hover elements — blue highlight + file paths in tooltip
47
- 3. Click to open source — cycles **TSX CSS TSX** when both exist
47
+ 3. Click to open the TSX source file in your IDE
48
48
  4. **Esc** — cancel pick mode
49
49
 
50
50
  | Shortcut | Action |
51
51
  |----------|--------|
52
- | Click | Open TSX (or cycle TSX/CSS) |
53
- | **Shift+C** | Open CSS/style directly |
52
+ | Click | Open TSX source |
54
53
  | Esc | Cancel pick |
55
54
 
56
55
  ## Exports
@@ -80,7 +79,7 @@ Control overlay colors (badge, tooltip, highlight). Presets or custom colors:
80
79
  | Preset | Look |
81
80
  |--------|------|
82
81
  | `'default'` | Dark slate + cyan accent |
83
- | `'light'` | White background + blue accent |
82
+ | `'light'` | White background + blue accent (also used when `theme` is omitted) |
84
83
  | `'dark'` | Black background + white/gray text |
85
84
  | `'blue'` | Navy background + light blue accent |
86
85
 
@@ -108,20 +107,6 @@ initSourceLocator({
108
107
  | `text` | Tooltip text |
109
108
  | `accent` | Borders, highlight, badge label |
110
109
 
111
- ## CSS Detection
112
-
113
- When an element has styles from a project `.css` file (e.g. `index.css` custom classes like `.glass`), the tooltip shows:
114
-
115
- ```
116
- components/DashboardStats.tsx
117
- CSS: index.css
118
- Click → TSX | next: CSS
119
- ```
120
-
121
- Click cycles between opening the TSX and CSS file.
122
-
123
- **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.
124
-
125
110
  ## MFE Safety
126
111
 
127
112
  - **Idempotent** — `window.__sourceLocator` guard: multiple bundles loading the locator mount only one overlay
@@ -162,7 +147,7 @@ sourceLocator()
162
147
  sourceLocator({ ides: ['vscode'] })
163
148
  ```
164
149
 
165
- The `ides` option controls which editors the server may open. The client always uses auto-detection.
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,11 +1,5 @@
1
- import { DEFAULT_IDE, nextClickTarget, parseSourceLocation, resolveTheme } from '../shared/index.js';
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;
@@ -14,6 +8,7 @@ function getSourceEl(target, attribute, host) {
14
8
  }
15
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,
@@ -22,27 +17,26 @@ async function openSourceInEditor(source, config) {
22
17
  });
23
18
  await fetch(`${config.endpoint}?${params.toString()}`);
24
19
  }
25
- function readElementSources(el, attribute) {
26
- const tsx = el.getAttribute(attribute) ?? undefined;
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 sources = EMPTY_SOURCES;
25
+ let componentSource;
36
26
  const ui = createLocatorOverlayUi(root, () => setPickMode(!pickMode), resolveTheme(config.theme));
37
27
  function setPickMode(active) {
28
+ if (active)
29
+ document.addEventListener('mousemove', onMouseMove);
30
+ else
31
+ document.removeEventListener('mousemove', onMouseMove);
38
32
  pickMode = active;
39
33
  ui.setPickActive(active);
40
34
  if (!active)
41
- sources = EMPTY_SOURCES;
35
+ componentSource = undefined;
42
36
  }
43
- const syncSources = (el) => {
37
+ const syncSource = (el) => {
44
38
  if (el !== ui.getActiveEl())
45
- sources = readElementSources(el, config.attribute);
39
+ componentSource = readComponentSource(el, config.attribute);
46
40
  };
47
41
  const updateHover = (target, x, y) => {
48
42
  const el = getSourceEl(target, config.attribute, host);
@@ -50,33 +44,15 @@ export function startPickController(root, host, config) {
50
44
  ui.removeTooltip();
51
45
  return;
52
46
  }
53
- syncSources(el);
54
- ui.showSourceTooltip(el, sources.tsx, sources.css, x, y);
47
+ syncSource(el);
48
+ ui.showSourceTooltip(el, componentSource, x, y);
55
49
  };
56
50
  const onMouseMove = (e) => {
57
- if (!pickMode) {
58
- ui.removeTooltip();
59
- return;
60
- }
61
51
  updateHover(e.target, e.clientX, e.clientY);
62
52
  };
63
- const onKeyDown = async (e) => {
64
- if (e.key === 'Escape' && pickMode) {
53
+ const onKeyDown = (e) => {
54
+ if (e.key === 'Escape' && pickMode)
65
55
  setPickMode(false);
66
- return;
67
- }
68
- if (e.shiftKey && e.key === 'C' && pickMode) {
69
- const el = ui.getActiveEl();
70
- if (!(el instanceof HTMLElement))
71
- return;
72
- syncSources(el);
73
- if (!sources.css) {
74
- ui.flashMessage('No CSS for this element');
75
- return;
76
- }
77
- await openSourceInEditor(sources.css, config);
78
- return;
79
- }
80
56
  };
81
57
  const onClick = async (e) => {
82
58
  if (!pickMode || host.contains(e.target))
@@ -84,24 +60,23 @@ export function startPickController(root, host, config) {
84
60
  e.preventDefault();
85
61
  e.stopPropagation();
86
62
  const el = getSourceEl(e.target, config.attribute, host);
87
- if (el)
88
- syncSources(el);
89
- const openSource = el && resolveOpenSource(sources);
90
- if (!el || !openSource) {
63
+ if (!el)
64
+ return;
65
+ syncSource(el);
66
+ if (!componentSource) {
91
67
  ui.flashMessage('No source for this element');
92
68
  return;
93
69
  }
94
- await openSourceInEditor(openSource, config);
95
- sources = { ...sources, clickTarget: nextClickTarget(sources.clickTarget, !!sources.css) };
96
- ui.showSourceTooltip(el, sources.tsx, sources.css, e.clientX, e.clientY);
70
+ await openSourceInEditor(componentSource, config);
71
+ ui.showSourceTooltip(el, componentSource, e.clientX, e.clientY);
97
72
  };
98
73
  document.addEventListener('keydown', onKeyDown);
99
- document.addEventListener('mousemove', onMouseMove);
100
74
  document.addEventListener('click', onClick, true);
101
75
  ui.mountBadge();
102
76
  return () => {
77
+ setPickMode(false);
103
78
  document.removeEventListener('keydown', onKeyDown);
104
- document.removeEventListener('mousemove', onMouseMove);
105
79
  document.removeEventListener('click', onClick, true);
80
+ ui.dispose();
106
81
  };
107
82
  }
@@ -2,9 +2,10 @@ import type { LocatorTheme } from '../shared/index.js';
2
2
  export declare function createLocatorOverlayUi(root: ShadowRoot, onTogglePick: () => void, theme: LocatorTheme): {
3
3
  mountBadge: () => void;
4
4
  setPickActive: (active: boolean) => void;
5
- showSourceTooltip: (el: Element, tsxSource: string | undefined, cssSource: string | undefined, x: number, y: number) => void;
5
+ showSourceTooltip: (el: Element, source: string | undefined, x: number, y: number) => void;
6
6
  flashMessage: (text: string) => void;
7
7
  removeTooltip: () => void;
8
+ dispose: () => void;
8
9
  getActiveEl: () => Element | null;
9
10
  };
10
11
  export type LocatorOverlayUi = ReturnType<typeof createLocatorOverlayUi>;
@@ -1,6 +1,7 @@
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;
@@ -49,8 +50,8 @@ export function createLocatorOverlayUi(root, onTogglePick, theme) {
49
50
  if (el)
50
51
  showHighlight(el);
51
52
  };
52
- const showSourceTooltip = (el, tsxSource, cssSource, x, y) => {
53
- showTooltip(buildTooltipText(tsxSource, cssSource), el, x, y);
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)
@@ -71,26 +72,29 @@ export function createLocatorOverlayUi(root, onTogglePick, theme) {
71
72
  document.body.style.cursor = active ? 'crosshair' : '';
72
73
  if (!badgeEl)
73
74
  return;
74
- badgeEl.textContent = badgeLabel(active);
75
+ badgeEl.textContent = active ? BADGE_LABEL_PICKING : BADGE_LABEL_IDLE;
75
76
  applyBadgeColors(active);
76
77
  if (!active)
77
78
  removeTooltip();
78
79
  };
80
+ const dispose = () => {
81
+ if (flashTimeout)
82
+ clearTimeout(flashTimeout);
83
+ flashTimeout = null;
84
+ removeTooltip();
85
+ };
79
86
  const mountBadge = () => {
80
87
  badgeEl = document.createElement('button');
81
88
  badgeEl.id = UI_IDS.badge;
82
89
  badgeEl.type = 'button';
83
- badgeEl.textContent = badgeLabel(false);
84
- Object.assign(badgeEl.style, LAYOUT.badge, {
85
- background: theme.badgeBackground,
86
- color: theme.badgeText,
87
- border: `1px solid ${theme.badgeBorder}`,
88
- });
90
+ badgeEl.textContent = BADGE_LABEL_IDLE;
91
+ Object.assign(badgeEl.style, LAYOUT.badge);
89
92
  badgeEl.addEventListener('click', (event) => {
90
93
  event.stopPropagation();
91
94
  onTogglePick();
92
95
  });
93
96
  root.appendChild(badgeEl);
97
+ applyBadgeColors(false);
94
98
  };
95
99
  return {
96
100
  mountBadge,
@@ -98,6 +102,7 @@ export function createLocatorOverlayUi(root, onTogglePick, theme) {
98
102
  showSourceTooltip,
99
103
  flashMessage,
100
104
  removeTooltip,
105
+ dispose,
101
106
  getActiveEl: () => activeEl,
102
107
  };
103
108
  }
@@ -1 +1 @@
1
- export declare function buildTooltipText(tsxSource: string | undefined, cssSource: string | undefined): string;
1
+ export declare function buildTooltipText(source: string | undefined): string;
@@ -1,23 +1,14 @@
1
1
  import { parseSourceLocation } from '../shared/index.js';
2
2
  const DEFAULT_LINE = '1';
3
- function formatSourceLabel(source, prefix) {
3
+ function formatSourceLabel(source) {
4
4
  const { file, line } = parseSourceLocation(source);
5
5
  const name = file.split('/').pop() ?? file;
6
- const label = line !== DEFAULT_LINE ? `${name}:${line}` : name;
7
- if (prefix)
8
- return `${prefix}: ${label}`;
9
- return label;
6
+ if (line !== DEFAULT_LINE)
7
+ return `${name}:${line}`;
8
+ return name;
10
9
  }
11
- export function buildTooltipText(tsxSource, cssSource) {
12
- const lines = [];
13
- if (tsxSource)
14
- lines.push(formatSourceLabel(tsxSource, 'TSX'));
15
- if (cssSource)
16
- lines.push(formatSourceLabel(cssSource, 'CSS'));
17
- if (!cssSource) {
18
- lines.push('Click → open TSX');
19
- return lines.join('\n');
20
- }
21
- lines.push('Click → open TSX', 'Shift+C → CSS');
22
- 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`;
23
14
  }
@@ -6,7 +6,6 @@ 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;
@@ -15,4 +14,3 @@ export declare function isLocatorIde(value: string): value is LocatorIde;
15
14
  export declare function resolveIde(value: string, allowed: LocatorIde[]): LocatorIde;
16
15
  export declare function parseSourceLocation(raw: string): SourceLocation;
17
16
  export declare function formatSourceLocation(loc: SourceLocation): string;
18
- export declare function nextClickTarget(current: ClickTarget, hasCss: boolean): ClickTarget;
@@ -25,12 +25,3 @@ export function parseSourceLocation(raw) {
25
25
  export function formatSourceLocation(loc) {
26
26
  return `${loc.file}:${loc.line}:${loc.col}`;
27
27
  }
28
- const CLICK_TARGET_TRANSITION = {
29
- tsx: 'css',
30
- css: 'tsx',
31
- };
32
- export function nextClickTarget(current, hasCss) {
33
- if (!hasCss)
34
- return 'tsx';
35
- return CLICK_TARGET_TRANSITION[current];
36
- }
@@ -19,23 +19,7 @@ function withAlpha(hex, alpha) {
19
19
  }
20
20
  function buildTheme(colors, preset) {
21
21
  const { background, text, accent } = colors;
22
- if (preset === 'light') {
23
- return {
24
- badgeBackground: background,
25
- badgeText: text,
26
- badgeActiveBackground: background,
27
- badgeActiveText: accent,
28
- badgeBorder: LIGHT_BADGE_BORDER,
29
- badgeActiveBorder: accent,
30
- tooltipBackground: background,
31
- tooltipText: text,
32
- tooltipBorder: LIGHT_BADGE_BORDER,
33
- highlightBorder: accent,
34
- highlightBackground: withAlpha(accent, HIGHLIGHT_ALPHA),
35
- highlightShadow: withAlpha(background, SHADOW_ALPHA),
36
- };
37
- }
38
- return {
22
+ const base = {
39
23
  badgeBackground: background,
40
24
  badgeText: accent,
41
25
  badgeActiveBackground: accent,
@@ -49,6 +33,16 @@ function buildTheme(colors, preset) {
49
33
  highlightBackground: withAlpha(accent, HIGHLIGHT_ALPHA),
50
34
  highlightShadow: withAlpha(background, SHADOW_ALPHA),
51
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
+ };
52
46
  }
53
47
  export function resolveTheme(input) {
54
48
  const lightBase = PRESET_COLORS.light;
@@ -26,15 +26,16 @@ 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.includes('/') || command.includes('\\')) && existsSync(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.includes('/') || path.includes('\\')) && existsSync(path));
38
+ const bundlePath = candidates.find((path) => isPathLike(path) && existsSync(path));
38
39
  if (bundlePath)
39
40
  return bundlePath;
40
41
  return command;
@@ -1,9 +1,9 @@
1
1
  import launch from 'launch-editor';
2
- import { resolveIde } from '../shared/index.js';
2
+ import { formatSourceLocation, resolveIde } from '../shared/index.js';
3
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 = `${loc.file}:${loc.line}:${loc.col}`;
6
+ const spec = formatSourceLocation(loc);
7
7
  if (ide === 'auto') {
8
8
  const resolved = resolveAutoEditor();
9
9
  if (resolved) {
@@ -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 = {}) {
@@ -55,6 +68,11 @@ function sourceLocator(options = {}) {
55
68
  },
56
69
  configureServer(server) {
57
70
  server.middlewares.use(config.endpoint, (req, res) => {
71
+ if (!isAllowedRequest(req)) {
72
+ res.writeHead(403);
73
+ res.end('forbidden');
74
+ return;
75
+ }
58
76
  const { file, line, col, ide } = readQuery(req.url ?? '', config.ides[0] ?? 'auto');
59
77
  if (!file) {
60
78
  res.writeHead(400);
@@ -63,6 +81,11 @@ function sourceLocator(options = {}) {
63
81
  }
64
82
  try {
65
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
+ }
66
89
  if (!existsSync(resolvedFile)) {
67
90
  res.writeHead(404);
68
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.1.4",
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",
@@ -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 +0,0 @@
1
- export declare function badgeLabel(picking: boolean): string;
@@ -1,5 +0,0 @@
1
- export function badgeLabel(picking) {
2
- if (picking)
3
- return 'Picking — Esc';
4
- return 'Locator';
5
- }
@@ -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, '&amp;')
8
- .replace(/</g, '&lt;')
9
- .replace(/>/g, '&gt;');
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
- }