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,188 @@
1
+ import type { ThemeMode, Theme } from '../types';
2
+
3
+ const STYLE_ID = '__ag-theme-vars__';
4
+ const OVERRIDE_STYLE_ID = '__ag-theme-overrides__';
5
+
6
+ const DARK_VARS = `
7
+ :root {
8
+ --ag-bg: #0f172a;
9
+ --ag-text: #e2e8f0;
10
+ --ag-text-muted: #64748b;
11
+ --ag-accent: #3b82f6;
12
+ --ag-accent-hover: #2563eb;
13
+ --ag-surface: #1e293b;
14
+ --ag-border: #334155;
15
+ --ag-overlay-border: #3b82f6;
16
+ --ag-overlay-bg: rgba(59, 130, 246, 0.1);
17
+ --ag-label-bg: #3b82f6;
18
+ --ag-label-text: #fff;
19
+ --ag-toast-bg: #0f172a;
20
+ --ag-toast-text: #e2e8f0;
21
+ --ag-toast-title: #fff;
22
+ --ag-toast-label: #64748b;
23
+ --ag-toast-shadow: rgba(0, 0, 0, 0.4);
24
+ --ag-toolbar-bg: #0f172a;
25
+ --ag-toolbar-text: #94a3b8;
26
+ --ag-toolbar-hover: #1e293b;
27
+ --ag-toolbar-active: #3b82f6;
28
+ --ag-toolbar-border: #1e293b;
29
+ --ag-toolbar-shadow: rgba(0, 0, 0, 0.5);
30
+ --ag-popover-bg: #0f172a;
31
+ --ag-popover-text: #e2e8f0;
32
+ --ag-popover-border: #1e293b;
33
+ --ag-popover-hover: #1e293b;
34
+ --ag-popover-shadow: rgba(0, 0, 0, 0.5);
35
+ }
36
+ `;
37
+
38
+ const LIGHT_VARS = `
39
+ :root {
40
+ --ag-bg: #ffffff;
41
+ --ag-text: #334155;
42
+ --ag-text-muted: #94a3b8;
43
+ --ag-accent: #2563eb;
44
+ --ag-accent-hover: #1d4ed8;
45
+ --ag-surface: #f1f5f9;
46
+ --ag-border: #e2e8f0;
47
+ --ag-overlay-border: #2563eb;
48
+ --ag-overlay-bg: rgba(37, 99, 235, 0.08);
49
+ --ag-label-bg: #2563eb;
50
+ --ag-label-text: #fff;
51
+ --ag-toast-bg: #ffffff;
52
+ --ag-toast-text: #334155;
53
+ --ag-toast-title: #0f172a;
54
+ --ag-toast-label: #94a3b8;
55
+ --ag-toast-shadow: rgba(0, 0, 0, 0.12);
56
+ --ag-toolbar-bg: #ffffff;
57
+ --ag-toolbar-text: #64748b;
58
+ --ag-toolbar-hover: #f1f5f9;
59
+ --ag-toolbar-active: #2563eb;
60
+ --ag-toolbar-border: #e2e8f0;
61
+ --ag-toolbar-shadow: rgba(0, 0, 0, 0.12);
62
+ --ag-popover-bg: #ffffff;
63
+ --ag-popover-text: #334155;
64
+ --ag-popover-border: #e2e8f0;
65
+ --ag-popover-hover: #f1f5f9;
66
+ --ag-popover-shadow: rgba(0, 0, 0, 0.12);
67
+ }
68
+ `;
69
+
70
+ /** Maps Theme interface fields to CSS variable names. */
71
+ const THEME_TO_VAR: Record<keyof Theme, string> = {
72
+ overlayBorderColor: '--ag-overlay-border',
73
+ overlayBgColor: '--ag-overlay-bg',
74
+ labelBgColor: '--ag-label-bg',
75
+ labelTextColor: '--ag-label-text',
76
+ toastBgColor: '--ag-toast-bg',
77
+ toastTextColor: '--ag-toast-text',
78
+ toolbarBgColor: '--ag-toolbar-bg',
79
+ toolbarTextColor: '--ag-toolbar-text',
80
+ toolbarAccentColor: '--ag-toolbar-active',
81
+ popoverBgColor: '--ag-popover-bg',
82
+ popoverTextColor: '--ag-popover-text',
83
+ popoverBorderColor: '--ag-popover-border',
84
+ };
85
+
86
+ export interface ThemeManager {
87
+ apply(mode: ThemeMode): void;
88
+ applyOverrides(theme: Partial<Theme>): void;
89
+ clearOverrides(): void;
90
+ dispose(): void;
91
+ }
92
+
93
+ export function createThemeManager(): ThemeManager {
94
+ let styleEl: HTMLStyleElement | null = null;
95
+ let overrideEl: HTMLStyleElement | null = null;
96
+ let currentMode: ThemeMode = 'dark';
97
+ let mediaQuery: MediaQueryList | null = null;
98
+ let mediaHandler: ((e: MediaQueryListEvent) => void) | null = null;
99
+
100
+ function getOrCreateStyle(): HTMLStyleElement {
101
+ if (styleEl) return styleEl;
102
+
103
+ const existing = document.getElementById(STYLE_ID) as HTMLStyleElement | null;
104
+ if (existing) {
105
+ styleEl = existing;
106
+ return styleEl;
107
+ }
108
+
109
+ styleEl = document.createElement('style');
110
+ styleEl.id = STYLE_ID;
111
+ document.head.appendChild(styleEl);
112
+ return styleEl;
113
+ }
114
+
115
+ function getOrCreateOverrideStyle(): HTMLStyleElement {
116
+ if (overrideEl) return overrideEl;
117
+
118
+ overrideEl = document.createElement('style');
119
+ overrideEl.id = OVERRIDE_STYLE_ID;
120
+ document.head.appendChild(overrideEl);
121
+ return overrideEl;
122
+ }
123
+
124
+ function resolveMode(mode: ThemeMode): 'dark' | 'light' {
125
+ if (mode !== 'system') return mode;
126
+ return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
127
+ }
128
+
129
+ function applyResolved(resolved: 'dark' | 'light'): void {
130
+ const el = getOrCreateStyle();
131
+ el.textContent = resolved === 'dark' ? DARK_VARS : LIGHT_VARS;
132
+ }
133
+
134
+ function detachMediaListener(): void {
135
+ if (mediaQuery && mediaHandler) {
136
+ mediaQuery.removeEventListener('change', mediaHandler);
137
+ }
138
+ mediaQuery = null;
139
+ mediaHandler = null;
140
+ }
141
+
142
+ return {
143
+ apply(mode: ThemeMode): void {
144
+ currentMode = mode;
145
+ detachMediaListener();
146
+
147
+ applyResolved(resolveMode(mode));
148
+
149
+ if (mode === 'system') {
150
+ mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
151
+ mediaHandler = () => applyResolved(resolveMode('system'));
152
+ mediaQuery.addEventListener('change', mediaHandler);
153
+ }
154
+ },
155
+
156
+ applyOverrides(theme: Partial<Theme>): void {
157
+ const vars: string[] = [];
158
+ for (const [key, varName] of Object.entries(THEME_TO_VAR)) {
159
+ const value = theme[key as keyof Theme];
160
+ if (value) {
161
+ vars.push(` ${varName}: ${value};`);
162
+ }
163
+ }
164
+
165
+ if (vars.length === 0) {
166
+ this.clearOverrides();
167
+ return;
168
+ }
169
+
170
+ const el = getOrCreateOverrideStyle();
171
+ el.textContent = ` :root {\n${vars.join('\n')}\n }`;
172
+ },
173
+
174
+ clearOverrides(): void {
175
+ overrideEl?.remove();
176
+ document.getElementById(OVERRIDE_STYLE_ID)?.remove();
177
+ overrideEl = null;
178
+ },
179
+
180
+ dispose(): void {
181
+ detachMediaListener();
182
+ styleEl?.remove();
183
+ document.getElementById(STYLE_ID)?.remove();
184
+ styleEl = null;
185
+ this.clearOverrides();
186
+ },
187
+ };
188
+ }
@@ -0,0 +1,29 @@
1
+ // All icons are 16x16, stroke-based with currentColor for theme compatibility
2
+
3
+ export const ICON_GRAB = `<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><path d="M5.5 7V3.5a1 1 0 0 1 2 0V7"/><path d="M7.5 6.5V2.5a1 1 0 0 1 2 0v4"/><path d="M9.5 7V3.5a1 1 0 0 1 2 0V8"/><path d="M5.5 7V5.5a1 1 0 0 0-2 0V9a4 4 0 0 0 4 4h1.5a4 4 0 0 0 4-4V6a1 1 0 0 0-2 0"/></svg>`;
4
+
5
+ export const ICON_HISTORY = `<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><circle cx="8" cy="8" r="6"/><path d="M8 4.5V8l2.5 1.5"/></svg>`;
6
+
7
+ export const ICON_ELLIPSIS = `<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><circle cx="4" cy="8" r="1.25"/><circle cx="8" cy="8" r="1.25"/><circle cx="12" cy="8" r="1.25"/></svg>`;
8
+
9
+ export const ICON_SUN = `<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" xmlns="http://www.w3.org/2000/svg"><circle cx="8" cy="8" r="3"/><path d="M8 1.5v1M8 13.5v1M1.5 8h1M13.5 8h1M3.4 3.4l.7.7M11.9 11.9l.7.7M3.4 12.6l.7-.7M11.9 4.1l.7-.7"/></svg>`;
10
+
11
+ export const ICON_MOON = `<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><path d="M13.5 8.5a5.5 5.5 0 0 1-7-7 5.5 5.5 0 1 0 7 7z"/></svg>`;
12
+
13
+ export const ICON_SYSTEM = `<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><rect x="2" y="2" width="12" height="9" rx="1"/><path d="M5.5 14h5"/><path d="M8 11v3"/></svg>`;
14
+
15
+ export const ICON_POWER = `<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><path d="M8 2v5"/><path d="M4.5 4a5.5 5.5 0 1 0 7 0"/></svg>`;
16
+
17
+ export const ICON_DISMISS = `<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" xmlns="http://www.w3.org/2000/svg"><path d="M4 4l8 8M12 4l-8 8"/></svg>`;
18
+
19
+ export const ICON_COPY = `<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><rect x="5.5" y="5.5" width="7" height="8" rx="1"/><path d="M3.5 10.5v-7a1 1 0 0 1 1-1h5"/></svg>`;
20
+
21
+ export const ICON_STYLES = `<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><path d="M13 1.5l-1.3 4.3a1 1 0 0 1-.7.7L6.7 7.8a1 1 0 0 0-.7.7L4.7 12.8a1 1 0 0 1-.7.7L1.5 14"/><path d="M7.5 5.5l3 3"/></svg>`;
22
+
23
+ export const ICON_CODE = `<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><path d="M5 4.5L1.5 8 5 11.5"/><path d="M11 4.5l3.5 3.5-3.5 3.5"/><path d="M9.5 2.5l-3 11"/></svg>`;
24
+
25
+ export const ICON_COMMENT = `<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><path d="M2 3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v7a1 1 0 0 1-1 1H5l-3 3V3z"/></svg>`;
26
+
27
+ export const ICON_TRASH = `<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><path d="M2.5 4.5h11"/><path d="M5.5 4.5V3a1 1 0 0 1 1-1h3a1 1 0 0 1 1 1v1.5"/><path d="M4 4.5l.5 8.5a1 1 0 0 0 1 1h5a1 1 0 0 0 1-1l.5-8.5"/></svg>`;
28
+
29
+ export const ICON_FREEZE = `<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><path d="M8 1v14"/><path d="M1 8h14"/><path d="M8 1l2 2M8 1L6 3"/><path d="M8 15l2-2M8 15l-2-2"/><path d="M1 8l2 2M1 8l2-2"/><path d="M15 8l-2 2M15 8l-2-2"/></svg>`;
@@ -0,0 +1,239 @@
1
+ import type { GrabState } from '../store';
2
+ import { Z_INDEX_TOOLBAR } from '../constants';
3
+ import { ICON_GRAB, ICON_HISTORY, ICON_ELLIPSIS, ICON_FREEZE, ICON_SUN, ICON_MOON, ICON_SYSTEM, ICON_POWER, ICON_DISMISS } from './toolbar-icons';
4
+
5
+ const TOOLBAR_ID = '__ag-toolbar__';
6
+ const STYLE_ID = '__ag-toolbar-styles__';
7
+
8
+ export interface ToolbarCallbacks {
9
+ onSelectionMode: () => void;
10
+ onHistory: () => void;
11
+ onActions: () => void;
12
+ onFreeze: () => void;
13
+ onThemeToggle: () => void;
14
+ onEnableToggle: () => void;
15
+ onDismiss: () => void;
16
+ }
17
+
18
+ export interface ToolbarRenderer {
19
+ show(): void;
20
+ hide(): void;
21
+ update(state: GrabState): void;
22
+ isToolbarElement(el: Element): boolean;
23
+ dispose(): void;
24
+ }
25
+
26
+ export function createToolbarRenderer(callbacks: ToolbarCallbacks): ToolbarRenderer {
27
+ let container: HTMLDivElement | null = null;
28
+ let leftGroup: HTMLDivElement | null = null;
29
+ let buttons: Record<string, HTMLButtonElement> = {};
30
+ let allElements = new Set<Element>();
31
+
32
+ function injectStyles(): void {
33
+ if (document.getElementById(STYLE_ID)) return;
34
+
35
+ const style = document.createElement('style');
36
+ style.id = STYLE_ID;
37
+ style.textContent = `
38
+ #${TOOLBAR_ID} {
39
+ position: fixed;
40
+ bottom: 20px;
41
+ left: 50%;
42
+ transform: translateX(-50%);
43
+ z-index: ${Z_INDEX_TOOLBAR};
44
+ display: flex;
45
+ align-items: center;
46
+ gap: 2px;
47
+ padding: 4px 6px;
48
+ background: var(--ag-toolbar-bg, #0f172a);
49
+ border: 1px solid var(--ag-toolbar-border, #1e293b);
50
+ border-radius: 24px;
51
+ box-shadow: 0 4px 16px var(--ag-toolbar-shadow, rgba(0, 0, 0, 0.5));
52
+ pointer-events: auto;
53
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
54
+ transition: opacity 0.2s ease, transform 0.2s ease;
55
+ }
56
+ #${TOOLBAR_ID}.ag-toolbar-hidden {
57
+ opacity: 0;
58
+ transform: translateX(-50%) translateY(20px);
59
+ pointer-events: none;
60
+ }
61
+ #${TOOLBAR_ID} button {
62
+ display: flex;
63
+ align-items: center;
64
+ justify-content: center;
65
+ width: 32px;
66
+ height: 32px;
67
+ border: none;
68
+ border-radius: 8px;
69
+ background: transparent;
70
+ color: var(--ag-toolbar-text, #94a3b8);
71
+ cursor: pointer;
72
+ padding: 0;
73
+ transition: background 0.15s ease, color 0.15s ease;
74
+ }
75
+ #${TOOLBAR_ID} button:hover {
76
+ background: var(--ag-toolbar-hover, #1e293b);
77
+ color: var(--ag-accent, #3b82f6);
78
+ }
79
+ #${TOOLBAR_ID} button.ag-btn-active {
80
+ color: var(--ag-toolbar-active, #3b82f6);
81
+ }
82
+ #${TOOLBAR_ID} button.ag-btn-disabled {
83
+ opacity: 0.4;
84
+ color: var(--ag-toolbar-text, #94a3b8);
85
+ }
86
+ #${TOOLBAR_ID} .ag-toolbar-divider {
87
+ width: 1px;
88
+ height: 20px;
89
+ background: var(--ag-toolbar-border, #1e293b);
90
+ margin: 0 4px;
91
+ flex-shrink: 0;
92
+ }
93
+ #${TOOLBAR_ID} .ag-toolbar-left {
94
+ display: flex;
95
+ align-items: center;
96
+ gap: 2px;
97
+ overflow: hidden;
98
+ max-width: 240px;
99
+ opacity: 1;
100
+ transition: max-width 0.25s ease, opacity 0.2s ease, margin 0.25s ease;
101
+ }
102
+ #${TOOLBAR_ID} .ag-toolbar-left.ag-toolbar-left-hidden {
103
+ max-width: 0;
104
+ opacity: 0;
105
+ pointer-events: none;
106
+ }
107
+ `;
108
+ document.head.appendChild(style);
109
+ }
110
+
111
+ function createButton(name: string, icon: string, title: string, onClick: () => void): HTMLButtonElement {
112
+ const btn = document.createElement('button');
113
+ btn.innerHTML = icon;
114
+ btn.title = title;
115
+ btn.setAttribute('aria-label', title);
116
+ btn.setAttribute('data-ag-btn', name);
117
+ btn.addEventListener('click', (e) => {
118
+ e.preventDefault();
119
+ e.stopPropagation();
120
+ onClick();
121
+ });
122
+ return btn;
123
+ }
124
+
125
+ function ensureContainer(): void {
126
+ if (container) return;
127
+
128
+ injectStyles();
129
+
130
+ container = document.createElement('div');
131
+ container.id = TOOLBAR_ID;
132
+ container.setAttribute('role', 'toolbar');
133
+ container.setAttribute('aria-label', 'Angular Grab toolbar');
134
+
135
+ buttons.selection = createButton('selection', ICON_GRAB, 'Selection mode', callbacks.onSelectionMode);
136
+ buttons.history = createButton('history', ICON_HISTORY, 'History', callbacks.onHistory);
137
+ buttons.actions = createButton('actions', ICON_ELLIPSIS, 'Actions', callbacks.onActions);
138
+ buttons.freeze = createButton('freeze', ICON_FREEZE, 'Freeze page (F)', callbacks.onFreeze);
139
+ buttons.theme = createButton('theme', ICON_SUN, 'Toggle theme', callbacks.onThemeToggle);
140
+ buttons.enable = createButton('enable', ICON_POWER, 'Enable/Disable', callbacks.onEnableToggle);
141
+ buttons.dismiss = createButton('dismiss', ICON_DISMISS, 'Dismiss toolbar', callbacks.onDismiss);
142
+
143
+ const divider = document.createElement('span');
144
+ divider.className = 'ag-toolbar-divider';
145
+
146
+ leftGroup = document.createElement('div');
147
+ leftGroup.className = 'ag-toolbar-left';
148
+ leftGroup.appendChild(buttons.selection);
149
+ leftGroup.appendChild(buttons.history);
150
+ leftGroup.appendChild(buttons.actions);
151
+ leftGroup.appendChild(buttons.freeze);
152
+ leftGroup.appendChild(divider);
153
+ container.appendChild(leftGroup);
154
+ container.appendChild(buttons.theme);
155
+ container.appendChild(buttons.enable);
156
+ container.appendChild(buttons.dismiss);
157
+
158
+ document.body.appendChild(container);
159
+
160
+ // Track all elements for isToolbarElement checks
161
+ allElements.clear();
162
+ allElements.add(container);
163
+ allElements.add(leftGroup);
164
+ allElements.add(divider);
165
+ for (const btn of Object.values(buttons)) {
166
+ allElements.add(btn);
167
+ }
168
+ }
169
+
170
+ return {
171
+ show(): void {
172
+ ensureContainer();
173
+ container!.classList.remove('ag-toolbar-hidden');
174
+ },
175
+
176
+ hide(): void {
177
+ if (container) {
178
+ container.classList.add('ag-toolbar-hidden');
179
+ }
180
+ },
181
+
182
+ update(state: GrabState): void {
183
+ if (!container) return;
184
+
185
+ // Selection mode active state
186
+ if (state.active) {
187
+ buttons.selection.classList.add('ag-btn-active');
188
+ } else {
189
+ buttons.selection.classList.remove('ag-btn-active');
190
+ }
191
+
192
+ // Theme icon
193
+ const mode = state.toolbar.themeMode;
194
+ const themeIcon = mode === 'dark' ? ICON_SUN : mode === 'light' ? ICON_MOON : ICON_SYSTEM;
195
+ const themeLabel = mode === 'dark' ? 'Switch to light mode' : mode === 'light' ? 'Switch to system theme' : 'Switch to dark mode';
196
+ buttons.theme.innerHTML = themeIcon;
197
+ buttons.theme.title = themeLabel;
198
+ buttons.theme.setAttribute('aria-label', themeLabel);
199
+
200
+ // Freeze button active state
201
+ if (state.frozen) {
202
+ buttons.freeze.classList.add('ag-btn-active');
203
+ } else {
204
+ buttons.freeze.classList.remove('ag-btn-active');
205
+ }
206
+
207
+ // Enable/disable — hide/show left side
208
+ if (state.options.enabled) {
209
+ buttons.enable.classList.add('ag-btn-active');
210
+ leftGroup?.classList.remove('ag-toolbar-left-hidden');
211
+ } else {
212
+ buttons.enable.classList.remove('ag-btn-active');
213
+ leftGroup?.classList.add('ag-toolbar-left-hidden');
214
+ }
215
+ },
216
+
217
+ isToolbarElement(el: Element): boolean {
218
+ if (allElements.has(el)) return true;
219
+
220
+ // Walk up to check if el is inside the toolbar (e.g. SVG children)
221
+ let current: Element | null = el;
222
+ while (current) {
223
+ if (current === container) return true;
224
+ if (current.id === TOOLBAR_ID) return true;
225
+ current = current.parentElement;
226
+ }
227
+ return false;
228
+ },
229
+
230
+ dispose(): void {
231
+ container?.remove();
232
+ document.getElementById(STYLE_ID)?.remove();
233
+ container = null;
234
+ leftGroup = null;
235
+ buttons = {};
236
+ allElements.clear();
237
+ },
238
+ };
239
+ }
@@ -0,0 +1,139 @@
1
+ export type ThemeMode = 'dark' | 'light' | 'system';
2
+
3
+ export type PendingAction =
4
+ | { type: 'copy-element' }
5
+ | { type: 'copy-styles' }
6
+ | { type: 'copy-html' }
7
+ | { type: 'comment' };
8
+
9
+ /** Serializable subset of ElementContext — no live DOM reference. */
10
+ export interface HistoryContext {
11
+ html: string;
12
+ componentName: string | null;
13
+ filePath: string | null;
14
+ line: number | null;
15
+ column: number | null;
16
+ componentStack: ComponentStackEntry[];
17
+ selector: string;
18
+ cssClasses: string[];
19
+ }
20
+
21
+ export interface HistoryEntry {
22
+ id: string;
23
+ context: HistoryContext;
24
+ snippet: string;
25
+ timestamp: number;
26
+ }
27
+
28
+ export interface ToolbarState {
29
+ visible: boolean;
30
+ themeMode: ThemeMode;
31
+ history: HistoryEntry[];
32
+ pendingAction: PendingAction | null;
33
+ }
34
+
35
+ export interface AngularGrabOptions {
36
+ /** Keyboard shortcut to activate. Default: "Meta+C" (Mac) / "Ctrl+C" (Win) */
37
+ activationKey: string;
38
+ /** Whether activation requires hold or toggle. Default: 'hold' */
39
+ activationMode: 'hold' | 'toggle';
40
+ /** Milliseconds to hold before activating in hold mode. Default: 0 */
41
+ keyHoldDuration: number;
42
+ /** Max lines of HTML to include in copied context. Default: 20 */
43
+ maxContextLines: number;
44
+ /** Master on/off switch. Default: true */
45
+ enabled: boolean;
46
+ /** Allow activation while focused in input/textarea. Default: false */
47
+ enableInInputs: boolean;
48
+ /** Only activate in dev mode. Default: true */
49
+ devOnly: boolean;
50
+ /** Show the floating mini toolbar. Default: true */
51
+ showToolbar: boolean;
52
+ /** Theme mode for all UI. Default: 'dark' */
53
+ themeMode: ThemeMode;
54
+ }
55
+
56
+ export interface ComponentStackEntry {
57
+ name: string;
58
+ filePath: string | null;
59
+ line: number | null;
60
+ column: number | null;
61
+ }
62
+
63
+ export interface ElementContext {
64
+ element: Element;
65
+ html: string;
66
+ componentName: string | null;
67
+ filePath: string | null;
68
+ line: number | null;
69
+ column: number | null;
70
+ componentStack: ComponentStackEntry[];
71
+ selector: string;
72
+ cssClasses: string[];
73
+ }
74
+
75
+ export interface PluginHooks {
76
+ onActivate?: () => void;
77
+ onDeactivate?: () => void;
78
+ onElementHover?: (element: Element) => void;
79
+ onElementSelect?: (context: ElementContext) => void;
80
+ onBeforeCopy?: (context: ElementContext) => void;
81
+ onCopySuccess?: (text: string, context: ElementContext, prompt?: string) => void;
82
+ onCopyError?: (error: Error) => void;
83
+ transformCopyContent?: (text: string, context: ElementContext) => string;
84
+ }
85
+
86
+ export interface Theme {
87
+ overlayBorderColor: string;
88
+ overlayBgColor: string;
89
+ labelBgColor: string;
90
+ labelTextColor: string;
91
+ toastBgColor: string;
92
+ toastTextColor: string;
93
+ toolbarBgColor: string;
94
+ toolbarTextColor: string;
95
+ toolbarAccentColor: string;
96
+ popoverBgColor: string;
97
+ popoverTextColor: string;
98
+ popoverBorderColor: string;
99
+ }
100
+
101
+ export interface Plugin {
102
+ name: string;
103
+ hooks?: PluginHooks;
104
+ theme?: Partial<Theme>;
105
+ options?: Partial<AngularGrabOptions>;
106
+ setup?: (api: AngularGrabAPI) => PluginCleanup | void;
107
+ }
108
+
109
+ export type PluginCleanup = () => void;
110
+
111
+ export type ComponentResolver = (element: Element) => {
112
+ name: string | null;
113
+ hostElement: Element | null;
114
+ stack?: Array<{ name: string; hostElement: Element | null }>;
115
+ } | null;
116
+
117
+ export type SourceResolver = (element: Element) => {
118
+ filePath: string | null;
119
+ line: number | null;
120
+ column: number | null;
121
+ } | null;
122
+
123
+ export interface AngularGrabAPI {
124
+ activate(): void;
125
+ deactivate(): void;
126
+ toggle(): void;
127
+ isActive(): boolean;
128
+ setOptions(opts: Partial<AngularGrabOptions>): void;
129
+ registerPlugin(plugin: Plugin): void;
130
+ unregisterPlugin(name: string): void;
131
+ setComponentResolver(resolver: ComponentResolver): void;
132
+ setSourceResolver(resolver: SourceResolver): void;
133
+ showToolbar(): void;
134
+ hideToolbar(): void;
135
+ setThemeMode(mode: ThemeMode): void;
136
+ getHistory(): HistoryEntry[];
137
+ clearHistory(): void;
138
+ dispose(): void;
139
+ }
@@ -0,0 +1,16 @@
1
+ export function escapeHtml(text: string): string {
2
+ const div = document.createElement('div');
3
+ div.textContent = text;
4
+ return div.innerHTML;
5
+ }
6
+
7
+ export function filterAngularClasses(classList: DOMTokenList): string[] {
8
+ return Array.from(classList).filter(c => !c.startsWith('ng-') && !c.startsWith('_ng'));
9
+ }
10
+
11
+ const NG_ATTR_RE = /\s_ng(host|content)-[a-z0-9-]+="[^"]*"/gi;
12
+ const NG_ATTR_EMPTY_RE = /\s_ng(host|content)-[a-z0-9-]+/gi;
13
+
14
+ export function cleanAngularAttrs(html: string): string {
15
+ return html.replace(NG_ATTR_RE, '').replace(NG_ATTR_EMPTY_RE, '');
16
+ }