angular-grab 0.1.0 → 0.1.2

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 (132) hide show
  1. package/README.md +215 -0
  2. package/examples/angular-19-app/.editorconfig +17 -0
  3. package/examples/angular-19-app/.vscode/extensions.json +4 -0
  4. package/examples/angular-19-app/.vscode/launch.json +20 -0
  5. package/examples/angular-19-app/.vscode/mcp.json +9 -0
  6. package/examples/angular-19-app/.vscode/tasks.json +42 -0
  7. package/examples/angular-19-app/README.md +59 -0
  8. package/examples/angular-19-app/angular.json +74 -0
  9. package/examples/angular-19-app/package.json +44 -0
  10. package/examples/angular-19-app/public/favicon.ico +0 -0
  11. package/examples/angular-19-app/src/app/app.config.ts +13 -0
  12. package/examples/angular-19-app/src/app/app.css +37 -0
  13. package/examples/angular-19-app/src/app/app.html +25 -0
  14. package/examples/angular-19-app/src/app/app.routes.ts +3 -0
  15. package/examples/angular-19-app/src/app/app.spec.ts +23 -0
  16. package/examples/angular-19-app/src/app/app.ts +12 -0
  17. package/examples/angular-19-app/src/app/button/button.component.ts +25 -0
  18. package/examples/angular-19-app/src/app/card/card.component.ts +33 -0
  19. package/examples/angular-19-app/src/app/header/header.component.ts +31 -0
  20. package/examples/angular-19-app/src/app/popover/popover.component.ts +133 -0
  21. package/examples/angular-19-app/src/index.html +13 -0
  22. package/examples/angular-19-app/src/main.ts +6 -0
  23. package/examples/angular-19-app/src/styles.css +1 -0
  24. package/examples/angular-19-app/tsconfig.app.json +15 -0
  25. package/examples/angular-19-app/tsconfig.json +33 -0
  26. package/examples/angular-19-app/tsconfig.spec.json +15 -0
  27. package/package.json +14 -111
  28. package/packages/angular-grab/package.json +96 -0
  29. package/packages/angular-grab/src/angular/__tests__/context-builder.test.ts +216 -0
  30. package/packages/angular-grab/src/angular/angular-grab.service.ts +62 -0
  31. package/packages/angular-grab/src/angular/index.ts +13 -0
  32. package/packages/angular-grab/src/angular/provide-angular-grab.ts +22 -0
  33. package/packages/angular-grab/src/angular/resolvers/component-resolver.ts +71 -0
  34. package/packages/angular-grab/src/angular/resolvers/context-builder.ts +86 -0
  35. package/packages/angular-grab/src/angular/resolvers/ng-utils.ts +14 -0
  36. package/packages/angular-grab/src/angular/resolvers/source-resolver.ts +61 -0
  37. package/packages/angular-grab/src/builder/__tests__/builder.test.ts +72 -0
  38. package/packages/angular-grab/src/builder/builders/application/index.ts +13 -0
  39. package/packages/angular-grab/src/builder/builders/dev-server/index.ts +9 -0
  40. package/packages/angular-grab/src/builder/index.ts +3 -0
  41. package/packages/angular-grab/src/cli/__tests__/cli.test.ts +239 -0
  42. package/packages/angular-grab/src/cli/commands/init.ts +106 -0
  43. package/packages/angular-grab/src/cli/index.ts +15 -0
  44. package/packages/angular-grab/src/cli/utils/detect-project.ts +78 -0
  45. package/packages/angular-grab/src/cli/utils/modify-angular-json.ts +42 -0
  46. package/packages/angular-grab/src/cli/utils/modify-app-config.ts +42 -0
  47. package/packages/angular-grab/src/core/__tests__/generate-snippet.test.ts +149 -0
  48. package/packages/angular-grab/src/core/__tests__/plugin-registry.test.ts +286 -0
  49. package/packages/angular-grab/src/core/__tests__/store.test.ts +118 -0
  50. package/packages/angular-grab/src/core/__tests__/utils.test.ts +85 -0
  51. package/packages/angular-grab/src/core/clipboard/copy.ts +104 -0
  52. package/packages/angular-grab/src/core/clipboard/generate-snippet.ts +38 -0
  53. package/packages/angular-grab/src/core/constants.ts +10 -0
  54. package/packages/angular-grab/src/core/grab.ts +596 -0
  55. package/packages/angular-grab/src/core/index.global.ts +13 -0
  56. package/packages/angular-grab/src/core/index.ts +19 -0
  57. package/packages/angular-grab/src/core/keyboard/keyboard-handler.ts +163 -0
  58. package/packages/angular-grab/src/core/overlay/crosshair.ts +107 -0
  59. package/packages/angular-grab/src/core/overlay/freeze-overlay.ts +239 -0
  60. package/packages/angular-grab/src/core/overlay/overlay-renderer.ts +180 -0
  61. package/packages/angular-grab/src/core/overlay/select-feedback.ts +108 -0
  62. package/packages/angular-grab/src/core/overlay/toast.ts +175 -0
  63. package/packages/angular-grab/src/core/picker/element-picker.ts +114 -0
  64. package/packages/angular-grab/src/core/plugins/plugin-registry.ts +83 -0
  65. package/packages/angular-grab/src/core/store.ts +52 -0
  66. package/packages/angular-grab/src/core/toolbar/actions-menu.ts +178 -0
  67. package/packages/angular-grab/src/core/toolbar/comment-popover.ts +235 -0
  68. package/packages/angular-grab/src/core/toolbar/copy-actions.ts +98 -0
  69. package/packages/angular-grab/src/core/toolbar/history-popover.ts +245 -0
  70. package/packages/angular-grab/src/core/toolbar/theme-manager.ts +188 -0
  71. package/packages/angular-grab/src/core/toolbar/toolbar-icons.ts +29 -0
  72. package/packages/angular-grab/src/core/toolbar/toolbar-renderer.ts +239 -0
  73. package/packages/angular-grab/src/core/types.ts +139 -0
  74. package/packages/angular-grab/src/core/utils.ts +16 -0
  75. package/packages/angular-grab/src/esbuild-plugin/__tests__/transform.test.ts +174 -0
  76. package/packages/angular-grab/src/esbuild-plugin/index.ts +3 -0
  77. package/packages/angular-grab/src/esbuild-plugin/plugin.ts +29 -0
  78. package/packages/angular-grab/src/esbuild-plugin/scan.ts +105 -0
  79. package/packages/angular-grab/src/esbuild-plugin/transform.ts +152 -0
  80. package/packages/angular-grab/src/vite-plugin/__tests__/plugin.test.ts +84 -0
  81. package/packages/angular-grab/src/vite-plugin/index.ts +19 -0
  82. package/packages/angular-grab/src/webpack-plugin/__tests__/plugin.test.ts +72 -0
  83. package/packages/angular-grab/src/webpack-plugin/index.ts +2 -0
  84. package/packages/angular-grab/src/webpack-plugin/loader.ts +15 -0
  85. package/packages/angular-grab/src/webpack-plugin/plugin.ts +20 -0
  86. package/packages/angular-grab/tsconfig.json +15 -0
  87. package/packages/angular-grab/tsup.config.ts +119 -0
  88. package/pnpm-workspace.yaml +3 -0
  89. package/turbo.json +21 -0
  90. package/dist/angular/index.d.ts +0 -151
  91. package/dist/angular/index.js +0 -2811
  92. package/dist/angular/index.js.map +0 -1
  93. package/dist/builder/builders/application/index.js +0 -143
  94. package/dist/builder/builders/application/index.js.map +0 -1
  95. package/dist/builder/builders/dev-server/index.js +0 -139
  96. package/dist/builder/builders/dev-server/index.js.map +0 -1
  97. package/dist/builder/index.js +0 -2
  98. package/dist/builder/index.js.map +0 -1
  99. package/dist/builder/package.json +0 -1
  100. package/dist/cli/index.js +0 -223
  101. package/dist/cli/index.js.map +0 -1
  102. package/dist/core/index.cjs +0 -2589
  103. package/dist/core/index.cjs.map +0 -1
  104. package/dist/core/index.d.cts +0 -139
  105. package/dist/core/index.d.ts +0 -139
  106. package/dist/core/index.global.js +0 -542
  107. package/dist/core/index.js +0 -2560
  108. package/dist/core/index.js.map +0 -1
  109. package/dist/esbuild-plugin/index.cjs +0 -239
  110. package/dist/esbuild-plugin/index.cjs.map +0 -1
  111. package/dist/esbuild-plugin/index.d.cts +0 -26
  112. package/dist/esbuild-plugin/index.d.ts +0 -26
  113. package/dist/esbuild-plugin/index.js +0 -200
  114. package/dist/esbuild-plugin/index.js.map +0 -1
  115. package/dist/vite-plugin/index.d.ts +0 -7
  116. package/dist/vite-plugin/index.js +0 -128
  117. package/dist/vite-plugin/index.js.map +0 -1
  118. package/dist/webpack-plugin/index.cjs +0 -54
  119. package/dist/webpack-plugin/index.cjs.map +0 -1
  120. package/dist/webpack-plugin/index.d.cts +0 -5
  121. package/dist/webpack-plugin/index.d.ts +0 -5
  122. package/dist/webpack-plugin/index.js +0 -23
  123. package/dist/webpack-plugin/index.js.map +0 -1
  124. package/dist/webpack-plugin/loader.cjs +0 -155
  125. package/dist/webpack-plugin/loader.cjs.map +0 -1
  126. package/dist/webpack-plugin/loader.d.cts +0 -3
  127. package/dist/webpack-plugin/loader.d.ts +0 -3
  128. package/dist/webpack-plugin/loader.js +0 -122
  129. package/dist/webpack-plugin/loader.js.map +0 -1
  130. /package/{builders.json → packages/angular-grab/builders.json} +0 -0
  131. /package/{dist → packages/angular-grab/src}/builder/builders/application/schema.json +0 -0
  132. /package/{dist → packages/angular-grab/src}/builder/builders/dev-server/schema.json +0 -0
@@ -0,0 +1,596 @@
1
+ import type {
2
+ AngularGrabOptions,
3
+ AngularGrabAPI,
4
+ Plugin,
5
+ ComponentResolver,
6
+ SourceResolver,
7
+ ElementContext,
8
+ HistoryContext,
9
+ HistoryEntry,
10
+ ThemeMode,
11
+ PendingAction,
12
+ } from './types';
13
+ import { createStore } from './store';
14
+ import { createOverlayRenderer } from './overlay/overlay-renderer';
15
+ import { createCrosshair } from './overlay/crosshair';
16
+ import { showToast, disposeToast } from './overlay/toast';
17
+ import { createElementPicker } from './picker/element-picker';
18
+ import { createKeyboardHandler, isMac } from './keyboard/keyboard-handler';
19
+ import { copyElement, buildElementContext } from './clipboard/copy';
20
+ import { createPluginRegistry } from './plugins/plugin-registry';
21
+ import { createThemeManager } from './toolbar/theme-manager';
22
+ import { createToolbarRenderer } from './toolbar/toolbar-renderer';
23
+ import { createHistoryPopover } from './toolbar/history-popover';
24
+ import { createActionsMenu } from './toolbar/actions-menu';
25
+ import { createCommentPopover } from './toolbar/comment-popover';
26
+ import { copyElementSnippet, copyElementHtml, copyElementStyles, copyWithComment } from './toolbar/copy-actions';
27
+ import { createFreezeOverlay } from './overlay/freeze-overlay';
28
+ import { showSelectFeedback, disposeFeedbackStyles } from './overlay/select-feedback';
29
+ import { TOOLBAR_TOAST_OFFSET } from './constants';
30
+
31
+ const MAX_HISTORY = 50;
32
+
33
+ function toHistoryContext(ctx: ElementContext): HistoryContext {
34
+ return {
35
+ html: ctx.html,
36
+ componentName: ctx.componentName,
37
+ filePath: ctx.filePath,
38
+ line: ctx.line,
39
+ column: ctx.column,
40
+ componentStack: ctx.componentStack,
41
+ selector: ctx.selector,
42
+ cssClasses: ctx.cssClasses,
43
+ };
44
+ }
45
+
46
+ function getDefaultOptions(): AngularGrabOptions {
47
+ return {
48
+ activationKey: isMac() ? 'Meta+C' : 'Ctrl+C',
49
+ activationMode: 'hold',
50
+ keyHoldDuration: 0,
51
+ maxContextLines: 20,
52
+ enabled: true,
53
+ enableInInputs: false,
54
+ devOnly: true,
55
+ showToolbar: true,
56
+ themeMode: 'dark',
57
+ };
58
+ }
59
+
60
+ export function init(options?: Partial<AngularGrabOptions>): AngularGrabAPI {
61
+ return createGrabInstance(options);
62
+ }
63
+
64
+ /** Check Angular's dev mode flag. Returns true if in dev mode or if the flag is absent. */
65
+ function isDevMode(): boolean {
66
+ try {
67
+ // Angular sets ngDevMode to false in production builds
68
+ const ng = (globalThis as any).ngDevMode;
69
+ return typeof ng === 'undefined' || !!ng;
70
+ } catch {
71
+ return true;
72
+ }
73
+ }
74
+
75
+ /** No-op API returned when devOnly is true and the app is in production. */
76
+ function createNoopApi(): AngularGrabAPI {
77
+ const noop = () => {};
78
+ return {
79
+ activate: noop,
80
+ deactivate: noop,
81
+ toggle: noop,
82
+ isActive: () => false,
83
+ setOptions: noop,
84
+ registerPlugin: noop,
85
+ unregisterPlugin: noop,
86
+ setComponentResolver: noop,
87
+ setSourceResolver: noop,
88
+ showToolbar: noop,
89
+ hideToolbar: noop,
90
+ setThemeMode: noop,
91
+ getHistory: () => [],
92
+ clearHistory: noop,
93
+ dispose: noop,
94
+ };
95
+ }
96
+
97
+ export function createGrabInstance(options?: Partial<AngularGrabOptions>): AngularGrabAPI {
98
+ const defaults = getDefaultOptions();
99
+ const merged: AngularGrabOptions = { ...defaults, ...options };
100
+
101
+ if (merged.devOnly && !isDevMode()) {
102
+ return createNoopApi();
103
+ }
104
+
105
+ const store = createStore(merged);
106
+ const overlay = createOverlayRenderer();
107
+ const crosshair = createCrosshair();
108
+ const freezeOverlay = createFreezeOverlay();
109
+ const pluginRegistry = createPluginRegistry();
110
+ const themeManager = createThemeManager();
111
+
112
+ let componentResolver: ComponentResolver | null = null;
113
+ let sourceResolver: SourceResolver | null = null;
114
+
115
+ // Per-instance state for last selected element (not in store to avoid serialization issues)
116
+ let lastSelectedElement: WeakRef<Element> | null = null;
117
+ let lastSelectedContext: ElementContext | null = null;
118
+ let idCounter = 0;
119
+
120
+ function nextId(): string {
121
+ return `ag-${++idCounter}-${Date.now()}`;
122
+ }
123
+
124
+ // Apply initial theme
125
+ themeManager.apply(store.state.toolbar.themeMode);
126
+
127
+ // Set toast bottom offset when toolbar is visible
128
+ updateToastOffset();
129
+
130
+ // --- Toolbar element check (aggregates all toolbar-related UI) ---
131
+ function isAnyToolbarElement(el: Element): boolean {
132
+ return toolbar.isToolbarElement(el)
133
+ || historyPopover.isPopoverElement(el)
134
+ || actionsMenu.isMenuElement(el)
135
+ || commentPopover.isPopoverElement(el)
136
+ || freezeOverlay.isFreezeElement(el);
137
+ }
138
+
139
+ // --- History management ---
140
+ function addHistoryEntry(context: ElementContext, snippet: string): void {
141
+ const entry: HistoryEntry = {
142
+ id: nextId(),
143
+ context: toHistoryContext(context),
144
+ snippet,
145
+ timestamp: Date.now(),
146
+ };
147
+
148
+ lastSelectedElement = new WeakRef(context.element);
149
+ lastSelectedContext = context;
150
+
151
+ const history = [entry, ...store.state.toolbar.history].slice(0, MAX_HISTORY);
152
+ store.state.toolbar = { ...store.state.toolbar, history };
153
+ }
154
+
155
+ /** Returns the live Element if it's still connected to the DOM. */
156
+ function getLastSelectedElement(): Element | null {
157
+ const el = lastSelectedElement?.deref() ?? null;
158
+ if (el && !el.isConnected) {
159
+ lastSelectedElement = null;
160
+ lastSelectedContext = null;
161
+ return null;
162
+ }
163
+ return el;
164
+ }
165
+
166
+ // --- Close all popovers ---
167
+ function closeAllPopovers(): void {
168
+ historyPopover.hide();
169
+ actionsMenu.hide();
170
+ commentPopover.hide();
171
+ }
172
+
173
+ // --- Pending action execution ---
174
+ async function executePendingAction(pending: PendingAction, element: Element): Promise<void> {
175
+ const context = buildElementContext(element, componentResolver, sourceResolver);
176
+ const maxLines = store.state.options.maxContextLines;
177
+
178
+ lastSelectedElement = new WeakRef(element);
179
+ lastSelectedContext = context;
180
+ store.state.toolbar = { ...store.state.toolbar, pendingAction: null };
181
+
182
+ switch (pending.type) {
183
+ case 'copy-element': {
184
+ const ok = await copyElementSnippet(context, maxLines, pluginRegistry);
185
+ if (ok) {
186
+ showSelectFeedback(element);
187
+ addHistoryEntry(context, '');
188
+ }
189
+ break;
190
+ }
191
+ case 'copy-styles':
192
+ await copyElementStyles(element);
193
+ break;
194
+ case 'copy-html':
195
+ await copyElementHtml(context, pluginRegistry);
196
+ break;
197
+ case 'comment':
198
+ commentPopover.show();
199
+ return; // Don't deactivate — user still needs to type
200
+ }
201
+ }
202
+
203
+ // --- Picker ---
204
+ const picker = createElementPicker({
205
+ overlay,
206
+ crosshair,
207
+ getComponentResolver: () => componentResolver,
208
+ getSourceResolver: () => sourceResolver,
209
+ isToolbarElement: isAnyToolbarElement,
210
+ getFreezeElement: () => freezeOverlay.getElement(),
211
+ onHover(element) {
212
+ store.state.hoveredElement = element;
213
+ if (element) {
214
+ pluginRegistry.callHook('onElementHover', element);
215
+ }
216
+ },
217
+ async onSelect(element) {
218
+ const pending = store.state.toolbar.pendingAction;
219
+
220
+ if (pending) {
221
+ await executePendingAction(pending, element);
222
+ // Comment flow keeps picker active
223
+ if (pending.type !== 'comment') {
224
+ doDeactivate();
225
+ }
226
+ return;
227
+ }
228
+
229
+ // Default copy flow
230
+ const result = await copyElement(element, {
231
+ getComponentResolver: () => componentResolver,
232
+ getSourceResolver: () => sourceResolver,
233
+ getMaxContextLines: () => store.state.options.maxContextLines,
234
+ pluginRegistry,
235
+ });
236
+
237
+ if (result) {
238
+ showSelectFeedback(element);
239
+ addHistoryEntry(result.context, result.snippet);
240
+ }
241
+ },
242
+ });
243
+
244
+ function doActivate(): void {
245
+ if (!store.state.options.enabled) return;
246
+ if (store.state.active) return;
247
+
248
+ // Show toolbar if it was dismissed
249
+ if (store.state.toolbar.visible === false && store.state.options.showToolbar) {
250
+ store.state.toolbar = { ...store.state.toolbar, visible: true };
251
+ toolbar.show();
252
+ toolbar.update(store.state);
253
+ }
254
+
255
+ store.state.active = true;
256
+ picker.activate();
257
+ pluginRegistry.callHook('onActivate');
258
+ toolbar.update(store.state);
259
+ }
260
+
261
+ function doDeactivate(): void {
262
+ if (!store.state.active) return;
263
+
264
+ store.state.active = false;
265
+ store.state.frozen = false;
266
+ freezeOverlay.hide();
267
+ store.state.toolbar = { ...store.state.toolbar, pendingAction: null };
268
+ picker.deactivate();
269
+ pluginRegistry.callHook('onDeactivate');
270
+ toolbar.update(store.state);
271
+ }
272
+
273
+ function toggleFreeze(): void {
274
+ store.state.frozen = !store.state.frozen;
275
+ if (store.state.frozen) {
276
+ freezeOverlay.show(store.state.hoveredElement);
277
+ } else {
278
+ freezeOverlay.hide();
279
+ }
280
+ toolbar.update(store.state);
281
+ }
282
+
283
+ // --- Toolbar ---
284
+ const toolbar = createToolbarRenderer({
285
+ onSelectionMode() {
286
+ closeAllPopovers();
287
+ if (store.state.active) {
288
+ doDeactivate();
289
+ } else {
290
+ doActivate();
291
+ }
292
+ },
293
+
294
+ onHistory() {
295
+ actionsMenu.hide();
296
+ commentPopover.hide();
297
+ if (historyPopover.isVisible()) {
298
+ historyPopover.hide();
299
+ } else {
300
+ historyPopover.show([...store.state.toolbar.history]);
301
+ }
302
+ },
303
+
304
+ onActions() {
305
+ historyPopover.hide();
306
+ commentPopover.hide();
307
+ if (actionsMenu.isVisible()) {
308
+ actionsMenu.hide();
309
+ } else {
310
+ actionsMenu.show();
311
+ }
312
+ },
313
+
314
+ onFreeze() {
315
+ closeAllPopovers();
316
+ if (!store.state.active) {
317
+ doActivate();
318
+ }
319
+ toggleFreeze();
320
+ },
321
+
322
+ onThemeToggle() {
323
+ const current = store.state.toolbar.themeMode;
324
+ const newMode: ThemeMode = current === 'dark' ? 'light' : current === 'light' ? 'system' : 'dark';
325
+ store.state.toolbar = { ...store.state.toolbar, themeMode: newMode };
326
+ themeManager.apply(newMode);
327
+ toolbar.update(store.state);
328
+ },
329
+
330
+ onEnableToggle() {
331
+ closeAllPopovers();
332
+ const newEnabled = !store.state.options.enabled;
333
+ store.state.options = { ...store.state.options, enabled: newEnabled };
334
+ if (!newEnabled) {
335
+ doDeactivate();
336
+ }
337
+ toolbar.update(store.state);
338
+ },
339
+
340
+ onDismiss() {
341
+ closeAllPopovers();
342
+ doDeactivate();
343
+ store.state.toolbar = { ...store.state.toolbar, visible: false };
344
+ toolbar.hide();
345
+ },
346
+ });
347
+
348
+ // --- History Popover ---
349
+ const historyPopover = createHistoryPopover({
350
+ async onEntryClick(entry: HistoryEntry) {
351
+ historyPopover.hide();
352
+ try {
353
+ await navigator.clipboard.writeText(entry.snippet);
354
+ showToast('Re-copied to clipboard', {
355
+ componentName: entry.context.componentName,
356
+ filePath: entry.context.filePath,
357
+ line: entry.context.line,
358
+ column: entry.context.column,
359
+ cssClasses: entry.context.cssClasses,
360
+ });
361
+ } catch {
362
+ // clipboard write failed silently
363
+ }
364
+ },
365
+ });
366
+
367
+ // --- Actions Menu ---
368
+ const actionsMenu = createActionsMenu({
369
+ onCopyElement() {
370
+ if (lastSelectedContext) {
371
+ copyElementSnippet(lastSelectedContext, store.state.options.maxContextLines, pluginRegistry);
372
+ } else {
373
+ store.state.toolbar = { ...store.state.toolbar, pendingAction: { type: 'copy-element' } };
374
+ doActivate();
375
+ }
376
+ },
377
+
378
+ onCopyStyles() {
379
+ const el = getLastSelectedElement();
380
+ if (el) {
381
+ copyElementStyles(el);
382
+ } else {
383
+ store.state.toolbar = { ...store.state.toolbar, pendingAction: { type: 'copy-styles' } };
384
+ doActivate();
385
+ }
386
+ },
387
+
388
+ onCopyHtml() {
389
+ if (lastSelectedContext) {
390
+ copyElementHtml(lastSelectedContext, pluginRegistry);
391
+ } else {
392
+ store.state.toolbar = { ...store.state.toolbar, pendingAction: { type: 'copy-html' } };
393
+ doActivate();
394
+ }
395
+ },
396
+
397
+ onComment() {
398
+ if (lastSelectedContext) {
399
+ commentPopover.show();
400
+ } else {
401
+ store.state.toolbar = { ...store.state.toolbar, pendingAction: { type: 'comment' } };
402
+ doActivate();
403
+ }
404
+ },
405
+
406
+ onClearHistory() {
407
+ lastSelectedContext = null;
408
+ lastSelectedElement = null;
409
+ store.state.toolbar = { ...store.state.toolbar, history: [] };
410
+ },
411
+ });
412
+
413
+ // --- Comment Popover ---
414
+ const commentPopover = createCommentPopover({
415
+ async onSubmit(comment: string) {
416
+ if (lastSelectedContext) {
417
+ await copyWithComment(lastSelectedContext, comment, store.state.options.maxContextLines, pluginRegistry);
418
+ }
419
+ if (store.state.active) {
420
+ doDeactivate();
421
+ }
422
+ },
423
+ onCancel() {
424
+ if (store.state.active) {
425
+ doDeactivate();
426
+ }
427
+ },
428
+ });
429
+
430
+ // --- Close popovers on outside click ---
431
+ function handleDocumentClick(e: MouseEvent): void {
432
+ const target = e.target as Element | null;
433
+ if (!target) return;
434
+
435
+ if (isAnyToolbarElement(target)) return;
436
+
437
+ // Close popovers if click is outside toolbar UI
438
+ if (historyPopover.isVisible() || actionsMenu.isVisible() || commentPopover.isVisible()) {
439
+ closeAllPopovers();
440
+ }
441
+ }
442
+ document.addEventListener('click', handleDocumentClick);
443
+
444
+ // --- Toast offset helper ---
445
+ function updateToastOffset(): void {
446
+ if (store.state.toolbar.visible) {
447
+ document.documentElement.style.setProperty('--ag-toast-bottom', TOOLBAR_TOAST_OFFSET);
448
+ } else {
449
+ document.documentElement.style.removeProperty('--ag-toast-bottom');
450
+ }
451
+ }
452
+
453
+ // --- Freeze key handler (F key during selection mode) ---
454
+ function handleFreezeKey(e: KeyboardEvent): void {
455
+ if (!store.state.active) return;
456
+ if (e.key.toLowerCase() !== 'f') return;
457
+ const tag = (e.target as Element)?.tagName;
458
+ if (tag === 'INPUT' || tag === 'TEXTAREA') return;
459
+ if ((e.target as HTMLElement)?.isContentEditable) return;
460
+
461
+ e.preventDefault();
462
+ toggleFreeze();
463
+ }
464
+ document.addEventListener('keydown', handleFreezeKey, true);
465
+
466
+ // --- Keyboard handler ---
467
+ const keyboard = createKeyboardHandler({
468
+ getActivationKey: () => store.state.options.activationKey,
469
+ getActivationMode: () => store.state.options.activationMode,
470
+ getKeyHoldDuration: () => store.state.options.keyHoldDuration,
471
+ getEnableInInputs: () => store.state.options.enableInInputs,
472
+ onActivate: doActivate,
473
+ onDeactivate: doDeactivate,
474
+ isActive: () => store.state.active,
475
+ });
476
+
477
+ // Build the API object so plugins can reference it
478
+ const api: AngularGrabAPI = {
479
+ activate: doActivate,
480
+ deactivate: doDeactivate,
481
+
482
+ toggle(): void {
483
+ if (store.state.active) {
484
+ doDeactivate();
485
+ } else {
486
+ doActivate();
487
+ }
488
+ },
489
+
490
+ isActive(): boolean {
491
+ return store.state.active;
492
+ },
493
+
494
+ setOptions(opts: Partial<AngularGrabOptions>): void {
495
+ store.state.options = { ...store.state.options, ...opts };
496
+ },
497
+
498
+ registerPlugin(plugin: Plugin): void {
499
+ if (plugin.options) {
500
+ store.state.options = { ...store.state.options, ...plugin.options };
501
+ }
502
+ if (plugin.theme) {
503
+ themeManager.applyOverrides(plugin.theme);
504
+ }
505
+ pluginRegistry.register(plugin, api);
506
+ },
507
+
508
+ unregisterPlugin(name: string): void {
509
+ pluginRegistry.unregister(name);
510
+ },
511
+
512
+ setComponentResolver(resolver: ComponentResolver): void {
513
+ componentResolver = resolver;
514
+ },
515
+
516
+ setSourceResolver(resolver: SourceResolver): void {
517
+ sourceResolver = resolver;
518
+ },
519
+
520
+ showToolbar(): void {
521
+ store.state.toolbar = { ...store.state.toolbar, visible: true };
522
+ toolbar.show();
523
+ toolbar.update(store.state);
524
+ updateToastOffset();
525
+ },
526
+
527
+ hideToolbar(): void {
528
+ closeAllPopovers();
529
+ store.state.toolbar = { ...store.state.toolbar, visible: false };
530
+ toolbar.hide();
531
+ updateToastOffset();
532
+ },
533
+
534
+ setThemeMode(mode: ThemeMode): void {
535
+ store.state.toolbar = { ...store.state.toolbar, themeMode: mode };
536
+ themeManager.apply(mode);
537
+ toolbar.update(store.state);
538
+ },
539
+
540
+ getHistory(): HistoryEntry[] {
541
+ return [...store.state.toolbar.history];
542
+ },
543
+
544
+ clearHistory(): void {
545
+ lastSelectedContext = null;
546
+ lastSelectedElement = null;
547
+ store.state.toolbar = { ...store.state.toolbar, history: [] };
548
+ },
549
+
550
+ dispose(): void {
551
+ doDeactivate();
552
+ document.removeEventListener('click', handleDocumentClick);
553
+ document.removeEventListener('keydown', handleFreezeKey, true);
554
+ keyboard.dispose();
555
+ picker.dispose();
556
+ overlay.dispose();
557
+ crosshair.dispose();
558
+ freezeOverlay.dispose();
559
+ disposeToast();
560
+ disposeFeedbackStyles();
561
+ pluginRegistry.dispose();
562
+ closeAllPopovers();
563
+ toolbar.dispose();
564
+ historyPopover.dispose();
565
+ actionsMenu.dispose();
566
+ commentPopover.dispose();
567
+ themeManager.dispose();
568
+ document.documentElement.style.removeProperty('--ag-toast-bottom');
569
+ },
570
+ };
571
+
572
+ // Start listening for keyboard shortcuts
573
+ if (store.state.options.enabled) {
574
+ keyboard.start();
575
+ }
576
+
577
+ // Toolbar starts hidden — it appears when selection mode is first activated
578
+ store.state.toolbar = { ...store.state.toolbar, visible: false };
579
+
580
+ // React to enabled option changes
581
+ store.subscribe((state, key) => {
582
+ if (key === 'options') {
583
+ if (state.options.enabled) {
584
+ keyboard.start();
585
+ } else {
586
+ keyboard.stop();
587
+ doDeactivate();
588
+ }
589
+ }
590
+ if (key === 'toolbar') {
591
+ updateToastOffset();
592
+ }
593
+ });
594
+
595
+ return api;
596
+ }
@@ -0,0 +1,13 @@
1
+ import { init } from './grab';
2
+
3
+ // Only initialize in development. The devOnly option (default: true) is checked,
4
+ // but the IIFE bundle should also guard against running in production builds
5
+ // where Angular's ngDevMode is explicitly false.
6
+ declare const ngDevMode: boolean | undefined;
7
+
8
+ const isDevMode = typeof ngDevMode === 'undefined' || !!ngDevMode;
9
+
10
+ if (isDevMode) {
11
+ const api = init();
12
+ (window as any).__ANGULAR_GRAB__ = api;
13
+ }
@@ -0,0 +1,19 @@
1
+ export { init, createGrabInstance } from './grab';
2
+ export { filterAngularClasses } from './utils';
3
+ export type {
4
+ AngularGrabOptions,
5
+ AngularGrabAPI,
6
+ ElementContext,
7
+ ComponentStackEntry,
8
+ Plugin,
9
+ PluginHooks,
10
+ PluginCleanup,
11
+ Theme,
12
+ ThemeMode,
13
+ HistoryContext,
14
+ HistoryEntry,
15
+ ToolbarState,
16
+ PendingAction,
17
+ ComponentResolver,
18
+ SourceResolver,
19
+ } from './types';