angular-grab-monorepo 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 +91 -0
- package/.playwright-mcp/page-2026-03-07T02-52-09-791Z.png +0 -0
- package/LICENSE +21 -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 +22 -0
- package/packages/angular-grab/builders.json +14 -0
- 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/application/schema.json +7 -0
- package/packages/angular-grab/src/builder/builders/dev-server/index.ts +9 -0
- package/packages/angular-grab/src/builder/builders/dev-server/schema.json +7 -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
|
@@ -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';
|