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.
Files changed (138) hide show
  1. package/.claude/settings.local.json +10 -0
  2. package/.playwright-mcp/console-2026-03-07T02-49-38-061Z.log +37 -0
  3. package/.playwright-mcp/console-2026-03-07T02-51-03-493Z.log +26 -0
  4. package/.playwright-mcp/console-2026-03-07T02-51-25-431Z.log +15 -0
  5. package/.playwright-mcp/console-2026-03-07T02-52-02-980Z.log +199 -0
  6. package/.playwright-mcp/page-2026-03-07T02-52-09-791Z.png +0 -0
  7. package/README.md +215 -0
  8. package/examples/angular-19-app/.editorconfig +17 -0
  9. package/examples/angular-19-app/.vscode/extensions.json +4 -0
  10. package/examples/angular-19-app/.vscode/launch.json +20 -0
  11. package/examples/angular-19-app/.vscode/mcp.json +9 -0
  12. package/examples/angular-19-app/.vscode/tasks.json +42 -0
  13. package/examples/angular-19-app/README.md +59 -0
  14. package/examples/angular-19-app/angular.json +79 -0
  15. package/examples/angular-19-app/package.json +42 -0
  16. package/examples/angular-19-app/public/favicon.ico +0 -0
  17. package/examples/angular-19-app/src/app/app.config.ts +13 -0
  18. package/examples/angular-19-app/src/app/app.css +37 -0
  19. package/examples/angular-19-app/src/app/app.html +25 -0
  20. package/examples/angular-19-app/src/app/app.routes.ts +3 -0
  21. package/examples/angular-19-app/src/app/app.spec.ts +23 -0
  22. package/examples/angular-19-app/src/app/app.ts +12 -0
  23. package/examples/angular-19-app/src/app/button/button.component.ts +25 -0
  24. package/examples/angular-19-app/src/app/card/card.component.ts +33 -0
  25. package/examples/angular-19-app/src/app/header/header.component.ts +31 -0
  26. package/examples/angular-19-app/src/app/popover/popover.component.ts +133 -0
  27. package/examples/angular-19-app/src/index.html +13 -0
  28. package/examples/angular-19-app/src/main.ts +6 -0
  29. package/examples/angular-19-app/src/styles.css +1 -0
  30. package/examples/angular-19-app/tsconfig.app.json +15 -0
  31. package/examples/angular-19-app/tsconfig.json +33 -0
  32. package/examples/angular-19-app/tsconfig.spec.json +15 -0
  33. package/package.json +14 -111
  34. package/packages/angular-grab/package.json +96 -0
  35. package/packages/angular-grab/src/angular/__tests__/context-builder.test.ts +216 -0
  36. package/packages/angular-grab/src/angular/angular-grab.service.ts +62 -0
  37. package/packages/angular-grab/src/angular/index.ts +13 -0
  38. package/packages/angular-grab/src/angular/provide-angular-grab.ts +22 -0
  39. package/packages/angular-grab/src/angular/resolvers/component-resolver.ts +71 -0
  40. package/packages/angular-grab/src/angular/resolvers/context-builder.ts +86 -0
  41. package/packages/angular-grab/src/angular/resolvers/ng-utils.ts +14 -0
  42. package/packages/angular-grab/src/angular/resolvers/source-resolver.ts +61 -0
  43. package/packages/angular-grab/src/builder/__tests__/builder.test.ts +72 -0
  44. package/packages/angular-grab/src/builder/builders/application/index.ts +13 -0
  45. package/packages/angular-grab/src/builder/builders/dev-server/index.ts +9 -0
  46. package/packages/angular-grab/src/builder/index.ts +3 -0
  47. package/packages/angular-grab/src/cli/__tests__/cli.test.ts +239 -0
  48. package/packages/angular-grab/src/cli/commands/init.ts +106 -0
  49. package/packages/angular-grab/src/cli/index.ts +15 -0
  50. package/packages/angular-grab/src/cli/utils/detect-project.ts +78 -0
  51. package/packages/angular-grab/src/cli/utils/modify-angular-json.ts +42 -0
  52. package/packages/angular-grab/src/cli/utils/modify-app-config.ts +42 -0
  53. package/packages/angular-grab/src/core/__tests__/generate-snippet.test.ts +149 -0
  54. package/packages/angular-grab/src/core/__tests__/plugin-registry.test.ts +286 -0
  55. package/packages/angular-grab/src/core/__tests__/store.test.ts +118 -0
  56. package/packages/angular-grab/src/core/__tests__/utils.test.ts +85 -0
  57. package/packages/angular-grab/src/core/clipboard/copy.ts +104 -0
  58. package/packages/angular-grab/src/core/clipboard/generate-snippet.ts +38 -0
  59. package/packages/angular-grab/src/core/constants.ts +10 -0
  60. package/packages/angular-grab/src/core/grab.ts +596 -0
  61. package/packages/angular-grab/src/core/index.global.ts +13 -0
  62. package/packages/angular-grab/src/core/index.ts +19 -0
  63. package/packages/angular-grab/src/core/keyboard/keyboard-handler.ts +163 -0
  64. package/packages/angular-grab/src/core/overlay/crosshair.ts +107 -0
  65. package/packages/angular-grab/src/core/overlay/freeze-overlay.ts +239 -0
  66. package/packages/angular-grab/src/core/overlay/overlay-renderer.ts +180 -0
  67. package/packages/angular-grab/src/core/overlay/select-feedback.ts +108 -0
  68. package/packages/angular-grab/src/core/overlay/toast.ts +175 -0
  69. package/packages/angular-grab/src/core/picker/element-picker.ts +114 -0
  70. package/packages/angular-grab/src/core/plugins/plugin-registry.ts +83 -0
  71. package/packages/angular-grab/src/core/store.ts +52 -0
  72. package/packages/angular-grab/src/core/toolbar/actions-menu.ts +178 -0
  73. package/packages/angular-grab/src/core/toolbar/comment-popover.ts +235 -0
  74. package/packages/angular-grab/src/core/toolbar/copy-actions.ts +98 -0
  75. package/packages/angular-grab/src/core/toolbar/history-popover.ts +245 -0
  76. package/packages/angular-grab/src/core/toolbar/theme-manager.ts +188 -0
  77. package/packages/angular-grab/src/core/toolbar/toolbar-icons.ts +29 -0
  78. package/packages/angular-grab/src/core/toolbar/toolbar-renderer.ts +239 -0
  79. package/packages/angular-grab/src/core/types.ts +139 -0
  80. package/packages/angular-grab/src/core/utils.ts +16 -0
  81. package/packages/angular-grab/src/esbuild-plugin/__tests__/transform.test.ts +174 -0
  82. package/packages/angular-grab/src/esbuild-plugin/index.ts +3 -0
  83. package/packages/angular-grab/src/esbuild-plugin/plugin.ts +29 -0
  84. package/packages/angular-grab/src/esbuild-plugin/scan.ts +105 -0
  85. package/packages/angular-grab/src/esbuild-plugin/transform.ts +152 -0
  86. package/packages/angular-grab/src/vite-plugin/__tests__/plugin.test.ts +84 -0
  87. package/packages/angular-grab/src/vite-plugin/index.ts +19 -0
  88. package/packages/angular-grab/src/webpack-plugin/__tests__/plugin.test.ts +72 -0
  89. package/packages/angular-grab/src/webpack-plugin/index.ts +2 -0
  90. package/packages/angular-grab/src/webpack-plugin/loader.ts +15 -0
  91. package/packages/angular-grab/src/webpack-plugin/plugin.ts +20 -0
  92. package/packages/angular-grab/tsconfig.json +15 -0
  93. package/packages/angular-grab/tsup.config.ts +119 -0
  94. package/pnpm-workspace.yaml +3 -0
  95. package/turbo.json +21 -0
  96. package/dist/angular/index.d.ts +0 -151
  97. package/dist/angular/index.js +0 -2811
  98. package/dist/angular/index.js.map +0 -1
  99. package/dist/builder/builders/application/index.js +0 -143
  100. package/dist/builder/builders/application/index.js.map +0 -1
  101. package/dist/builder/builders/dev-server/index.js +0 -139
  102. package/dist/builder/builders/dev-server/index.js.map +0 -1
  103. package/dist/builder/index.js +0 -2
  104. package/dist/builder/index.js.map +0 -1
  105. package/dist/builder/package.json +0 -1
  106. package/dist/cli/index.js +0 -223
  107. package/dist/cli/index.js.map +0 -1
  108. package/dist/core/index.cjs +0 -2589
  109. package/dist/core/index.cjs.map +0 -1
  110. package/dist/core/index.d.cts +0 -139
  111. package/dist/core/index.d.ts +0 -139
  112. package/dist/core/index.global.js +0 -542
  113. package/dist/core/index.js +0 -2560
  114. package/dist/core/index.js.map +0 -1
  115. package/dist/esbuild-plugin/index.cjs +0 -239
  116. package/dist/esbuild-plugin/index.cjs.map +0 -1
  117. package/dist/esbuild-plugin/index.d.cts +0 -26
  118. package/dist/esbuild-plugin/index.d.ts +0 -26
  119. package/dist/esbuild-plugin/index.js +0 -200
  120. package/dist/esbuild-plugin/index.js.map +0 -1
  121. package/dist/vite-plugin/index.d.ts +0 -7
  122. package/dist/vite-plugin/index.js +0 -128
  123. package/dist/vite-plugin/index.js.map +0 -1
  124. package/dist/webpack-plugin/index.cjs +0 -54
  125. package/dist/webpack-plugin/index.cjs.map +0 -1
  126. package/dist/webpack-plugin/index.d.cts +0 -5
  127. package/dist/webpack-plugin/index.d.ts +0 -5
  128. package/dist/webpack-plugin/index.js +0 -23
  129. package/dist/webpack-plugin/index.js.map +0 -1
  130. package/dist/webpack-plugin/loader.cjs +0 -155
  131. package/dist/webpack-plugin/loader.cjs.map +0 -1
  132. package/dist/webpack-plugin/loader.d.cts +0 -3
  133. package/dist/webpack-plugin/loader.d.ts +0 -3
  134. package/dist/webpack-plugin/loader.js +0 -122
  135. package/dist/webpack-plugin/loader.js.map +0 -1
  136. /package/{builders.json → packages/angular-grab/builders.json} +0 -0
  137. /package/{dist → packages/angular-grab/src}/builder/builders/application/schema.json +0 -0
  138. /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 &amp; bar');
8
+ });
9
+
10
+ it('escapes angle brackets', () => {
11
+ expect(escapeHtml('<div>')).toBe('&lt;div&gt;');
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
+ '&lt;a href="x"&gt;&amp;&lt;/a&gt;',
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';