angular-grab 0.1.1 → 0.1.3
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/.claude/settings.local.json +10 -0
- package/.playwright-mcp/console-2026-03-07T02-49-38-061Z.log +37 -0
- package/.playwright-mcp/console-2026-03-07T02-51-03-493Z.log +26 -0
- package/.playwright-mcp/console-2026-03-07T02-51-25-431Z.log +15 -0
- package/.playwright-mcp/console-2026-03-07T02-52-02-980Z.log +199 -0
- package/.playwright-mcp/page-2026-03-07T02-52-09-791Z.png +0 -0
- package/README.md +215 -0
- package/examples/angular-19-app/.editorconfig +17 -0
- package/examples/angular-19-app/.vscode/extensions.json +4 -0
- package/examples/angular-19-app/.vscode/launch.json +20 -0
- package/examples/angular-19-app/.vscode/mcp.json +9 -0
- package/examples/angular-19-app/.vscode/tasks.json +42 -0
- package/examples/angular-19-app/README.md +59 -0
- package/examples/angular-19-app/angular.json +79 -0
- package/examples/angular-19-app/package.json +42 -0
- package/examples/angular-19-app/public/favicon.ico +0 -0
- package/examples/angular-19-app/src/app/app.config.ts +13 -0
- package/examples/angular-19-app/src/app/app.css +37 -0
- package/examples/angular-19-app/src/app/app.html +25 -0
- package/examples/angular-19-app/src/app/app.routes.ts +3 -0
- package/examples/angular-19-app/src/app/app.spec.ts +23 -0
- package/examples/angular-19-app/src/app/app.ts +12 -0
- package/examples/angular-19-app/src/app/button/button.component.ts +25 -0
- package/examples/angular-19-app/src/app/card/card.component.ts +33 -0
- package/examples/angular-19-app/src/app/header/header.component.ts +31 -0
- package/examples/angular-19-app/src/app/popover/popover.component.ts +133 -0
- package/examples/angular-19-app/src/index.html +13 -0
- package/examples/angular-19-app/src/main.ts +6 -0
- package/examples/angular-19-app/src/styles.css +1 -0
- package/examples/angular-19-app/tsconfig.app.json +15 -0
- package/examples/angular-19-app/tsconfig.json +33 -0
- package/examples/angular-19-app/tsconfig.spec.json +15 -0
- package/package.json +14 -111
- package/packages/angular-grab/package.json +96 -0
- package/packages/angular-grab/src/angular/__tests__/context-builder.test.ts +216 -0
- package/packages/angular-grab/src/angular/angular-grab.service.ts +62 -0
- package/packages/angular-grab/src/angular/index.ts +13 -0
- package/packages/angular-grab/src/angular/provide-angular-grab.ts +22 -0
- package/packages/angular-grab/src/angular/resolvers/component-resolver.ts +71 -0
- package/packages/angular-grab/src/angular/resolvers/context-builder.ts +86 -0
- package/packages/angular-grab/src/angular/resolvers/ng-utils.ts +14 -0
- package/packages/angular-grab/src/angular/resolvers/source-resolver.ts +61 -0
- package/packages/angular-grab/src/builder/__tests__/builder.test.ts +72 -0
- package/packages/angular-grab/src/builder/builders/application/index.ts +13 -0
- package/packages/angular-grab/src/builder/builders/dev-server/index.ts +9 -0
- package/packages/angular-grab/src/builder/index.ts +3 -0
- package/packages/angular-grab/src/cli/__tests__/cli.test.ts +239 -0
- package/packages/angular-grab/src/cli/commands/init.ts +106 -0
- package/packages/angular-grab/src/cli/index.ts +15 -0
- package/packages/angular-grab/src/cli/utils/detect-project.ts +78 -0
- package/packages/angular-grab/src/cli/utils/modify-angular-json.ts +42 -0
- package/packages/angular-grab/src/cli/utils/modify-app-config.ts +42 -0
- package/packages/angular-grab/src/core/__tests__/generate-snippet.test.ts +149 -0
- package/packages/angular-grab/src/core/__tests__/plugin-registry.test.ts +286 -0
- package/packages/angular-grab/src/core/__tests__/store.test.ts +118 -0
- package/packages/angular-grab/src/core/__tests__/utils.test.ts +85 -0
- package/packages/angular-grab/src/core/clipboard/copy.ts +104 -0
- package/packages/angular-grab/src/core/clipboard/generate-snippet.ts +38 -0
- package/packages/angular-grab/src/core/constants.ts +10 -0
- package/packages/angular-grab/src/core/grab.ts +596 -0
- package/packages/angular-grab/src/core/index.global.ts +13 -0
- package/packages/angular-grab/src/core/index.ts +19 -0
- package/packages/angular-grab/src/core/keyboard/keyboard-handler.ts +163 -0
- package/packages/angular-grab/src/core/overlay/crosshair.ts +107 -0
- package/packages/angular-grab/src/core/overlay/freeze-overlay.ts +239 -0
- package/packages/angular-grab/src/core/overlay/overlay-renderer.ts +180 -0
- package/packages/angular-grab/src/core/overlay/select-feedback.ts +108 -0
- package/packages/angular-grab/src/core/overlay/toast.ts +175 -0
- package/packages/angular-grab/src/core/picker/element-picker.ts +114 -0
- package/packages/angular-grab/src/core/plugins/plugin-registry.ts +83 -0
- package/packages/angular-grab/src/core/store.ts +52 -0
- package/packages/angular-grab/src/core/toolbar/actions-menu.ts +178 -0
- package/packages/angular-grab/src/core/toolbar/comment-popover.ts +235 -0
- package/packages/angular-grab/src/core/toolbar/copy-actions.ts +98 -0
- package/packages/angular-grab/src/core/toolbar/history-popover.ts +245 -0
- package/packages/angular-grab/src/core/toolbar/theme-manager.ts +188 -0
- package/packages/angular-grab/src/core/toolbar/toolbar-icons.ts +29 -0
- package/packages/angular-grab/src/core/toolbar/toolbar-renderer.ts +239 -0
- package/packages/angular-grab/src/core/types.ts +139 -0
- package/packages/angular-grab/src/core/utils.ts +16 -0
- package/packages/angular-grab/src/esbuild-plugin/__tests__/transform.test.ts +174 -0
- package/packages/angular-grab/src/esbuild-plugin/index.ts +3 -0
- package/packages/angular-grab/src/esbuild-plugin/plugin.ts +29 -0
- package/packages/angular-grab/src/esbuild-plugin/scan.ts +105 -0
- package/packages/angular-grab/src/esbuild-plugin/transform.ts +152 -0
- package/packages/angular-grab/src/vite-plugin/__tests__/plugin.test.ts +84 -0
- package/packages/angular-grab/src/vite-plugin/index.ts +19 -0
- package/packages/angular-grab/src/webpack-plugin/__tests__/plugin.test.ts +72 -0
- package/packages/angular-grab/src/webpack-plugin/index.ts +2 -0
- package/packages/angular-grab/src/webpack-plugin/loader.ts +15 -0
- package/packages/angular-grab/src/webpack-plugin/plugin.ts +20 -0
- package/packages/angular-grab/tsconfig.json +15 -0
- package/packages/angular-grab/tsup.config.ts +119 -0
- package/pnpm-workspace.yaml +3 -0
- package/turbo.json +21 -0
- package/dist/angular/index.d.ts +0 -151
- package/dist/angular/index.js +0 -2811
- package/dist/angular/index.js.map +0 -1
- package/dist/builder/builders/application/index.js +0 -143
- package/dist/builder/builders/application/index.js.map +0 -1
- package/dist/builder/builders/dev-server/index.js +0 -139
- package/dist/builder/builders/dev-server/index.js.map +0 -1
- package/dist/builder/index.js +0 -2
- package/dist/builder/index.js.map +0 -1
- package/dist/builder/package.json +0 -1
- package/dist/cli/index.js +0 -223
- package/dist/cli/index.js.map +0 -1
- package/dist/core/index.cjs +0 -2589
- package/dist/core/index.cjs.map +0 -1
- package/dist/core/index.d.cts +0 -139
- package/dist/core/index.d.ts +0 -139
- package/dist/core/index.global.js +0 -542
- package/dist/core/index.js +0 -2560
- package/dist/core/index.js.map +0 -1
- package/dist/esbuild-plugin/index.cjs +0 -239
- package/dist/esbuild-plugin/index.cjs.map +0 -1
- package/dist/esbuild-plugin/index.d.cts +0 -26
- package/dist/esbuild-plugin/index.d.ts +0 -26
- package/dist/esbuild-plugin/index.js +0 -200
- package/dist/esbuild-plugin/index.js.map +0 -1
- package/dist/vite-plugin/index.d.ts +0 -7
- package/dist/vite-plugin/index.js +0 -128
- package/dist/vite-plugin/index.js.map +0 -1
- package/dist/webpack-plugin/index.cjs +0 -54
- package/dist/webpack-plugin/index.cjs.map +0 -1
- package/dist/webpack-plugin/index.d.cts +0 -5
- package/dist/webpack-plugin/index.d.ts +0 -5
- package/dist/webpack-plugin/index.js +0 -23
- package/dist/webpack-plugin/index.js.map +0 -1
- package/dist/webpack-plugin/loader.cjs +0 -155
- package/dist/webpack-plugin/loader.cjs.map +0 -1
- package/dist/webpack-plugin/loader.d.cts +0 -3
- package/dist/webpack-plugin/loader.d.ts +0 -3
- package/dist/webpack-plugin/loader.js +0 -122
- package/dist/webpack-plugin/loader.js.map +0 -1
- /package/{builders.json → packages/angular-grab/builders.json} +0 -0
- /package/{dist → packages/angular-grab/src}/builder/builders/application/schema.json +0 -0
- /package/{dist → packages/angular-grab/src}/builder/builders/dev-server/schema.json +0 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { createStore } from '../store';
|
|
3
|
+
import type { AngularGrabOptions } from '../types';
|
|
4
|
+
|
|
5
|
+
function makeOptions(overrides: Partial<AngularGrabOptions> = {}): AngularGrabOptions {
|
|
6
|
+
return {
|
|
7
|
+
activationKey: 'Meta+C',
|
|
8
|
+
activationMode: 'hold',
|
|
9
|
+
keyHoldDuration: 0,
|
|
10
|
+
maxContextLines: 20,
|
|
11
|
+
enabled: true,
|
|
12
|
+
enableInInputs: false,
|
|
13
|
+
devOnly: true,
|
|
14
|
+
showToolbar: true,
|
|
15
|
+
themeMode: 'dark',
|
|
16
|
+
...overrides,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe('createStore', () => {
|
|
21
|
+
it('creates state with correct default values', () => {
|
|
22
|
+
const opts = makeOptions();
|
|
23
|
+
const store = createStore(opts);
|
|
24
|
+
|
|
25
|
+
expect(store.state.active).toBe(false);
|
|
26
|
+
expect(store.state.frozen).toBe(false);
|
|
27
|
+
expect(store.state.hoveredElement).toBeNull();
|
|
28
|
+
expect(store.state.options).toBe(opts);
|
|
29
|
+
expect(store.state.toolbar.visible).toBe(true);
|
|
30
|
+
expect(store.state.toolbar.themeMode).toBe('dark');
|
|
31
|
+
expect(store.state.toolbar.history).toEqual([]);
|
|
32
|
+
expect(store.state.toolbar.pendingAction).toBeNull();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('respects showToolbar=false in options', () => {
|
|
36
|
+
const store = createStore(makeOptions({ showToolbar: false }));
|
|
37
|
+
expect(store.state.toolbar.visible).toBe(false);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('respects themeMode in options', () => {
|
|
41
|
+
const store = createStore(makeOptions({ themeMode: 'light' }));
|
|
42
|
+
expect(store.state.toolbar.themeMode).toBe('light');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('notifies listeners on state changes', () => {
|
|
46
|
+
const store = createStore(makeOptions());
|
|
47
|
+
const listener = vi.fn();
|
|
48
|
+
|
|
49
|
+
store.subscribe(listener);
|
|
50
|
+
store.state.active = true;
|
|
51
|
+
|
|
52
|
+
expect(listener).toHaveBeenCalledTimes(1);
|
|
53
|
+
expect(listener).toHaveBeenCalledWith(store.state, 'active');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('does not notify listeners when value has not changed', () => {
|
|
57
|
+
const store = createStore(makeOptions());
|
|
58
|
+
const listener = vi.fn();
|
|
59
|
+
|
|
60
|
+
store.subscribe(listener);
|
|
61
|
+
store.state.active = false; // same as default
|
|
62
|
+
|
|
63
|
+
expect(listener).not.toHaveBeenCalled();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('subscribe returns an unsubscribe function that works', () => {
|
|
67
|
+
const store = createStore(makeOptions());
|
|
68
|
+
const listener = vi.fn();
|
|
69
|
+
|
|
70
|
+
const unsub = store.subscribe(listener);
|
|
71
|
+
store.state.active = true;
|
|
72
|
+
expect(listener).toHaveBeenCalledTimes(1);
|
|
73
|
+
|
|
74
|
+
unsub();
|
|
75
|
+
store.state.active = false;
|
|
76
|
+
expect(listener).toHaveBeenCalledTimes(1); // no additional call
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('supports multiple listeners', () => {
|
|
80
|
+
const store = createStore(makeOptions());
|
|
81
|
+
const listener1 = vi.fn();
|
|
82
|
+
const listener2 = vi.fn();
|
|
83
|
+
|
|
84
|
+
store.subscribe(listener1);
|
|
85
|
+
store.subscribe(listener2);
|
|
86
|
+
store.state.frozen = true;
|
|
87
|
+
|
|
88
|
+
expect(listener1).toHaveBeenCalledTimes(1);
|
|
89
|
+
expect(listener2).toHaveBeenCalledTimes(1);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('unsubscribing one listener does not affect others', () => {
|
|
93
|
+
const store = createStore(makeOptions());
|
|
94
|
+
const listener1 = vi.fn();
|
|
95
|
+
const listener2 = vi.fn();
|
|
96
|
+
|
|
97
|
+
const unsub1 = store.subscribe(listener1);
|
|
98
|
+
store.subscribe(listener2);
|
|
99
|
+
|
|
100
|
+
unsub1();
|
|
101
|
+
store.state.active = true;
|
|
102
|
+
|
|
103
|
+
expect(listener1).not.toHaveBeenCalled();
|
|
104
|
+
expect(listener2).toHaveBeenCalledTimes(1);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('passes the proxy state and changed key to listeners', () => {
|
|
108
|
+
const store = createStore(makeOptions());
|
|
109
|
+
const listener = vi.fn();
|
|
110
|
+
|
|
111
|
+
store.subscribe(listener);
|
|
112
|
+
store.state.frozen = true;
|
|
113
|
+
|
|
114
|
+
const [receivedState, receivedKey] = listener.mock.calls[0];
|
|
115
|
+
expect(receivedKey).toBe('frozen');
|
|
116
|
+
expect(receivedState.frozen).toBe(true);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { describe, it, expect } from 'vitest';
|
|
3
|
+
import { escapeHtml, cleanAngularAttrs } from '../utils';
|
|
4
|
+
|
|
5
|
+
describe('escapeHtml', () => {
|
|
6
|
+
it('escapes ampersands', () => {
|
|
7
|
+
expect(escapeHtml('foo & bar')).toBe('foo & bar');
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('escapes angle brackets', () => {
|
|
11
|
+
expect(escapeHtml('<div>')).toBe('<div>');
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('passes through double quotes (not special in text nodes)', () => {
|
|
15
|
+
// innerHTML only escapes <, >, & in text content — not quotes
|
|
16
|
+
expect(escapeHtml('"hello"')).toBe('"hello"');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('passes through single quotes (not special in text nodes)', () => {
|
|
20
|
+
expect(escapeHtml("it's")).toBe("it's");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('escapes multiple special characters together', () => {
|
|
24
|
+
expect(escapeHtml('<a href="x">&</a>')).toBe(
|
|
25
|
+
'<a href="x">&</a>',
|
|
26
|
+
);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('returns empty string for empty input', () => {
|
|
30
|
+
expect(escapeHtml('')).toBe('');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('returns plain text unchanged', () => {
|
|
34
|
+
expect(escapeHtml('hello world')).toBe('hello world');
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('cleanAngularAttrs', () => {
|
|
39
|
+
it('removes _nghost attributes with values', () => {
|
|
40
|
+
const html = '<div _nghost-abc-123=""></div>';
|
|
41
|
+
expect(cleanAngularAttrs(html)).toBe('<div></div>');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('removes _ngcontent attributes with values', () => {
|
|
45
|
+
const html = '<span _ngcontent-xyz-456=""></span>';
|
|
46
|
+
expect(cleanAngularAttrs(html)).toBe('<span></span>');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('removes _nghost attributes without values', () => {
|
|
50
|
+
const html = '<div _nghost-abc-123></div>';
|
|
51
|
+
expect(cleanAngularAttrs(html)).toBe('<div></div>');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('removes _ngcontent attributes without values', () => {
|
|
55
|
+
const html = '<span _ngcontent-xyz-456></span>';
|
|
56
|
+
expect(cleanAngularAttrs(html)).toBe('<span></span>');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('removes multiple Angular attributes from one tag', () => {
|
|
60
|
+
const html = '<div _nghost-abc-123="" _ngcontent-xyz-456="">text</div>';
|
|
61
|
+
expect(cleanAngularAttrs(html)).toBe('<div>text</div>');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('removes Angular attributes from nested elements', () => {
|
|
65
|
+
const html =
|
|
66
|
+
'<div _nghost-a-1=""><span _ngcontent-b-2="">hi</span></div>';
|
|
67
|
+
expect(cleanAngularAttrs(html)).toBe('<div><span>hi</span></div>');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('preserves non-Angular attributes', () => {
|
|
71
|
+
const html = '<div class="foo" id="bar" _nghost-abc-123="">text</div>';
|
|
72
|
+
expect(cleanAngularAttrs(html)).toBe(
|
|
73
|
+
'<div class="foo" id="bar">text</div>',
|
|
74
|
+
);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('returns unchanged HTML when no Angular attributes present', () => {
|
|
78
|
+
const html = '<div class="test">hello</div>';
|
|
79
|
+
expect(cleanAngularAttrs(html)).toBe(html);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('handles empty string', () => {
|
|
83
|
+
expect(cleanAngularAttrs('')).toBe('');
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { ElementContext, ComponentStackEntry, ComponentResolver, SourceResolver } from '../types';
|
|
2
|
+
import type { PluginRegistry } from '../plugins/plugin-registry';
|
|
3
|
+
import { generateSnippet } from './generate-snippet';
|
|
4
|
+
import { showToast, type ToastDetail } from '../overlay/toast';
|
|
5
|
+
import { filterAngularClasses } from '../utils';
|
|
6
|
+
|
|
7
|
+
export interface CopyDeps {
|
|
8
|
+
getComponentResolver: () => ComponentResolver | null;
|
|
9
|
+
getSourceResolver: () => SourceResolver | null;
|
|
10
|
+
getMaxContextLines: () => number;
|
|
11
|
+
pluginRegistry: PluginRegistry;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function buildSelector(el: Element): string {
|
|
15
|
+
const tag = el.tagName.toLowerCase();
|
|
16
|
+
const id = el.id ? `#${el.id}` : '';
|
|
17
|
+
const classes = filterAngularClasses(el.classList)
|
|
18
|
+
.map((c) => `.${c}`)
|
|
19
|
+
.join('');
|
|
20
|
+
return `${tag}${id}${classes}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function getCssClasses(el: Element): string[] {
|
|
24
|
+
return filterAngularClasses(el.classList);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function buildElementContext(
|
|
28
|
+
element: Element,
|
|
29
|
+
componentResolver: ComponentResolver | null,
|
|
30
|
+
sourceResolver: SourceResolver | null,
|
|
31
|
+
): ElementContext {
|
|
32
|
+
const compResult = componentResolver?.(element);
|
|
33
|
+
const srcResult = sourceResolver?.(element);
|
|
34
|
+
|
|
35
|
+
// Build component stack from resolver result
|
|
36
|
+
const componentStack: ComponentStackEntry[] = [];
|
|
37
|
+
if (compResult?.stack) {
|
|
38
|
+
for (const entry of compResult.stack) {
|
|
39
|
+
let filePath: string | null = null;
|
|
40
|
+
let line: number | null = null;
|
|
41
|
+
let column: number | null = null;
|
|
42
|
+
|
|
43
|
+
if (entry.hostElement && sourceResolver) {
|
|
44
|
+
const src = sourceResolver(entry.hostElement);
|
|
45
|
+
if (src) {
|
|
46
|
+
filePath = src.filePath;
|
|
47
|
+
line = src.line;
|
|
48
|
+
column = src.column;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
componentStack.push({ name: entry.name, filePath, line, column });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
element,
|
|
58
|
+
html: element.outerHTML,
|
|
59
|
+
componentName: compResult?.name ?? null,
|
|
60
|
+
filePath: srcResult?.filePath ?? null,
|
|
61
|
+
line: srcResult?.line ?? null,
|
|
62
|
+
column: srcResult?.column ?? null,
|
|
63
|
+
componentStack,
|
|
64
|
+
selector: buildSelector(element),
|
|
65
|
+
cssClasses: getCssClasses(element),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface CopyResult {
|
|
70
|
+
context: ElementContext;
|
|
71
|
+
snippet: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function copyElement(element: Element, deps: CopyDeps): Promise<CopyResult | null> {
|
|
75
|
+
const context = buildElementContext(
|
|
76
|
+
element,
|
|
77
|
+
deps.getComponentResolver(),
|
|
78
|
+
deps.getSourceResolver(),
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
deps.pluginRegistry.callHook('onElementSelect', context);
|
|
82
|
+
deps.pluginRegistry.callHook('onBeforeCopy', context);
|
|
83
|
+
|
|
84
|
+
let snippet = generateSnippet(context, deps.getMaxContextLines());
|
|
85
|
+
snippet = deps.pluginRegistry.callTransformHook(snippet, context);
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
await navigator.clipboard.writeText(snippet);
|
|
89
|
+
const detail: ToastDetail = {
|
|
90
|
+
componentName: context.componentName,
|
|
91
|
+
filePath: context.filePath,
|
|
92
|
+
line: context.line,
|
|
93
|
+
column: context.column,
|
|
94
|
+
cssClasses: context.cssClasses,
|
|
95
|
+
};
|
|
96
|
+
showToast('Copied to clipboard', detail);
|
|
97
|
+
deps.pluginRegistry.callHook('onCopySuccess', snippet, context, undefined);
|
|
98
|
+
return { context, snippet };
|
|
99
|
+
} catch (err) {
|
|
100
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
101
|
+
deps.pluginRegistry.callHook('onCopyError', error);
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { ElementContext } from '../types';
|
|
2
|
+
import { cleanAngularAttrs } from '../utils';
|
|
3
|
+
|
|
4
|
+
function truncateHtml(html: string, maxLines: number): string {
|
|
5
|
+
const lines = html.split('\n');
|
|
6
|
+
if (lines.length <= maxLines) return html;
|
|
7
|
+
|
|
8
|
+
return lines.slice(0, maxLines).join('\n') + '\n ...';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function formatLocation(name: string | null, filePath: string | null, line: number | null, column: number | null): string {
|
|
12
|
+
let locationLine = '';
|
|
13
|
+
if (name) locationLine += `in ${name}`;
|
|
14
|
+
if (filePath) {
|
|
15
|
+
const loc = filePath +
|
|
16
|
+
(line != null ? `:${line}` : '') +
|
|
17
|
+
(line != null && column != null ? `:${column}` : '');
|
|
18
|
+
locationLine += locationLine ? ` at ${loc}` : `at ${loc}`;
|
|
19
|
+
}
|
|
20
|
+
return locationLine;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function generateSnippet(context: ElementContext, maxContextLines: number): string {
|
|
24
|
+
const cleaned = cleanAngularAttrs(context.html);
|
|
25
|
+
const truncated = truncateHtml(cleaned, maxContextLines);
|
|
26
|
+
|
|
27
|
+
const parts: string[] = [truncated];
|
|
28
|
+
|
|
29
|
+
if (context.componentStack.length > 0) {
|
|
30
|
+
for (const entry of context.componentStack) {
|
|
31
|
+
parts.push(formatLocation(entry.name, entry.filePath, entry.line, entry.column));
|
|
32
|
+
}
|
|
33
|
+
} else if (context.componentName || context.filePath) {
|
|
34
|
+
parts.push(formatLocation(context.componentName, context.filePath, context.line, context.column));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return parts.join('\n');
|
|
38
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export const Z_INDEX_FREEZE = 2147483644;
|
|
2
|
+
export const Z_INDEX_CROSSHAIR = 2147483645;
|
|
3
|
+
export const Z_INDEX_OVERLAY = 2147483646;
|
|
4
|
+
export const Z_INDEX_LABEL = 2147483647;
|
|
5
|
+
export const Z_INDEX_TOOLBAR = 2147483646;
|
|
6
|
+
export const Z_INDEX_TOAST = 2147483647;
|
|
7
|
+
export const Z_INDEX_POPOVER = 2147483647;
|
|
8
|
+
|
|
9
|
+
export const TOOLBAR_TOAST_OFFSET = '72px';
|
|
10
|
+
export const TOOLBAR_POPOVER_OFFSET = '68px';
|