@ng-annotate/angular 0.2.1

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.
@@ -0,0 +1,18 @@
1
+ import { OnDestroy } from '@angular/core';
2
+ import { BehaviorSubject } from 'rxjs';
3
+ import type { Session, Annotation } from './types.js';
4
+ export declare class BridgeService implements OnDestroy {
5
+ readonly session$: BehaviorSubject<Session | null>;
6
+ readonly annotations$: BehaviorSubject<Annotation[]>;
7
+ readonly connected$: BehaviorSubject<boolean>;
8
+ private readonly zone;
9
+ private ws;
10
+ private reconnectTimer;
11
+ init(): void;
12
+ private connect;
13
+ createAnnotation(payload: Record<string, unknown>): void;
14
+ replyToAnnotation(id: string, message: string): void;
15
+ deleteAnnotation(id: string): void;
16
+ private send;
17
+ ngOnDestroy(): void;
18
+ }
@@ -0,0 +1,85 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { Injectable, NgZone, inject } from '@angular/core';
8
+ import { BehaviorSubject } from 'rxjs';
9
+ let BridgeService = class BridgeService {
10
+ constructor() {
11
+ this.session$ = new BehaviorSubject(null);
12
+ this.annotations$ = new BehaviorSubject([]);
13
+ this.connected$ = new BehaviorSubject(false);
14
+ this.zone = inject(NgZone);
15
+ this.ws = null;
16
+ this.reconnectTimer = null;
17
+ }
18
+ init() {
19
+ this.connect();
20
+ }
21
+ connect() {
22
+ const wsUrl = `ws://${location.host}/__annotate`;
23
+ this.ws = new WebSocket(wsUrl);
24
+ this.ws.onopen = () => {
25
+ this.zone.run(() => {
26
+ this.connected$.next(true);
27
+ });
28
+ };
29
+ this.ws.onmessage = (event) => {
30
+ this.zone.run(() => {
31
+ try {
32
+ const data = JSON.parse(event.data);
33
+ if (data.type === 'session:created') {
34
+ this.session$.next(data.session);
35
+ }
36
+ else if (data.type === 'annotations:sync') {
37
+ this.annotations$.next(data.annotations);
38
+ }
39
+ else if (data.type === 'annotation:created') {
40
+ const annotation = data.annotation;
41
+ const current = this.annotations$.getValue();
42
+ this.annotations$.next([...current, annotation]);
43
+ }
44
+ }
45
+ catch {
46
+ // ignore malformed messages
47
+ }
48
+ });
49
+ };
50
+ this.ws.onclose = () => {
51
+ this.zone.run(() => {
52
+ this.connected$.next(false);
53
+ this.reconnectTimer = setTimeout(() => { this.connect(); }, 3000);
54
+ });
55
+ };
56
+ this.ws.onerror = (event) => {
57
+ console.warn('[ng-annotate] WebSocket error', event);
58
+ };
59
+ }
60
+ createAnnotation(payload) {
61
+ this.send({ type: 'annotation:create', payload });
62
+ }
63
+ replyToAnnotation(id, message) {
64
+ this.send({ type: 'annotation:reply', id, message });
65
+ }
66
+ deleteAnnotation(id) {
67
+ this.send({ type: 'annotation:delete', id });
68
+ }
69
+ send(msg) {
70
+ if (this.ws?.readyState === WebSocket.OPEN) {
71
+ this.ws.send(JSON.stringify(msg));
72
+ }
73
+ }
74
+ ngOnDestroy() {
75
+ if (this.reconnectTimer !== null) {
76
+ clearTimeout(this.reconnectTimer);
77
+ this.reconnectTimer = null;
78
+ }
79
+ this.ws?.close();
80
+ }
81
+ };
82
+ BridgeService = __decorate([
83
+ Injectable()
84
+ ], BridgeService);
85
+ export { BridgeService };
@@ -0,0 +1,6 @@
1
+ export { NgAnnotateModule } from './ng-annotate.module.js';
2
+ export { provideNgAnnotate } from './provide-ng-annotate.js';
3
+ export { InspectorService } from './inspector.service.js';
4
+ export { BridgeService } from './bridge.service.js';
5
+ export { OverlayComponent } from './overlay/overlay.component.js';
6
+ export * from './types.js';
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ export { NgAnnotateModule } from './ng-annotate.module.js';
2
+ export { provideNgAnnotate } from './provide-ng-annotate.js';
3
+ export { InspectorService } from './inspector.service.js';
4
+ export { BridgeService } from './bridge.service.js';
5
+ export { OverlayComponent } from './overlay/overlay.component.js';
6
+ export * from './types.js';
@@ -0,0 +1,10 @@
1
+ import type { ComponentContext } from './types.js';
2
+ export declare class InspectorService {
3
+ getComponentContext(element: Element): ComponentContext | null;
4
+ private findNearestComponent;
5
+ private getSelector;
6
+ private getInputs;
7
+ private buildTreePath;
8
+ private snapshot;
9
+ private resolveFilePaths;
10
+ }
@@ -0,0 +1,130 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { Injectable } from '@angular/core';
8
+ const DOM_SNAPSHOT_MAX = 5000;
9
+ let InspectorService = class InspectorService {
10
+ getComponentContext(element) {
11
+ const component = this.findNearestComponent(element);
12
+ if (!component)
13
+ return null;
14
+ const componentName = component.constructor.name;
15
+ const { component: componentFilePath, template: templateFilePath } = this.resolveFilePaths(componentName);
16
+ const selector = this.getSelector(component);
17
+ const inputs = this.getInputs(component);
18
+ const domSnapshot = this.snapshot(element);
19
+ const componentTreePath = this.buildTreePath(element);
20
+ return {
21
+ componentName,
22
+ componentFilePath,
23
+ ...(templateFilePath ? { templateFilePath } : {}),
24
+ selector,
25
+ inputs,
26
+ domSnapshot,
27
+ componentTreePath,
28
+ };
29
+ }
30
+ findNearestComponent(element) {
31
+ let current = element;
32
+ while (current) {
33
+ try {
34
+ const comp = ng.getComponent(current);
35
+ if (comp)
36
+ return comp;
37
+ }
38
+ catch {
39
+ // ignore
40
+ }
41
+ current = current.parentElement;
42
+ }
43
+ return null;
44
+ }
45
+ getSelector(component) {
46
+ try {
47
+ const cmp = component.constructor.ɵcmp;
48
+ if (!cmp?.selectors?.length)
49
+ return 'unknown-selector';
50
+ const first = cmp.selectors[0];
51
+ if (!first.length)
52
+ return 'unknown-selector';
53
+ // Element selector: [['app-foo']]
54
+ // Attribute selector: [['', 'appFoo', '']]
55
+ if (first[0] === '') {
56
+ // Attribute selector — find non-empty entries
57
+ const attrParts = [];
58
+ for (let i = 1; i < first.length; i += 2) {
59
+ if (typeof first[i] === 'string' && first[i]) {
60
+ attrParts.push(`[${String(first[i])}]`);
61
+ }
62
+ }
63
+ return attrParts.join('') || 'unknown-selector';
64
+ }
65
+ return String(first[0]);
66
+ }
67
+ catch {
68
+ return 'unknown-selector';
69
+ }
70
+ }
71
+ getInputs(component) {
72
+ try {
73
+ const cmp = component.constructor.ɵcmp;
74
+ if (!cmp?.inputs)
75
+ return {};
76
+ const result = {};
77
+ for (const [propName] of Object.entries(cmp.inputs)) {
78
+ if (typeof propName === 'symbol')
79
+ continue;
80
+ if (propName.startsWith('ɵ'))
81
+ continue;
82
+ result[propName] = component[propName];
83
+ }
84
+ return result;
85
+ }
86
+ catch {
87
+ return {};
88
+ }
89
+ }
90
+ buildTreePath(element) {
91
+ const path = [];
92
+ let current = element.parentElement;
93
+ while (current) {
94
+ try {
95
+ const comp = ng.getComponent(current);
96
+ if (comp) {
97
+ path.unshift(comp.constructor.name);
98
+ }
99
+ }
100
+ catch {
101
+ // ignore
102
+ }
103
+ current = current.parentElement;
104
+ }
105
+ return path;
106
+ }
107
+ snapshot(element) {
108
+ const html = element.outerHTML;
109
+ if (html.length <= DOM_SNAPSHOT_MAX)
110
+ return html;
111
+ return html.slice(0, DOM_SNAPSHOT_MAX) + '<!-- truncated -->';
112
+ }
113
+ resolveFilePaths(componentName) {
114
+ try {
115
+ const manifest = window
116
+ .__NG_ANNOTATE_MANIFEST__;
117
+ const entry = manifest?.[componentName];
118
+ if (entry)
119
+ return entry;
120
+ }
121
+ catch {
122
+ // ignore
123
+ }
124
+ return { component: `(unresolved: ${componentName})` };
125
+ }
126
+ };
127
+ InspectorService = __decorate([
128
+ Injectable()
129
+ ], InspectorService);
130
+ export { InspectorService };
@@ -0,0 +1,2 @@
1
+ export declare class NgAnnotateModule {
2
+ }
@@ -0,0 +1,33 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { NgModule, isDevMode, provideAppInitializer, inject, ApplicationRef, EnvironmentInjector, createComponent, } from '@angular/core';
8
+ import { InspectorService } from './inspector.service.js';
9
+ import { BridgeService } from './bridge.service.js';
10
+ import { OverlayComponent } from './overlay/overlay.component.js';
11
+ let NgAnnotateModule = class NgAnnotateModule {
12
+ };
13
+ NgAnnotateModule = __decorate([
14
+ NgModule({
15
+ providers: isDevMode()
16
+ ? [
17
+ InspectorService,
18
+ BridgeService,
19
+ provideAppInitializer(() => {
20
+ const bridge = inject(BridgeService);
21
+ const appRef = inject(ApplicationRef);
22
+ const envInjector = inject(EnvironmentInjector);
23
+ bridge.init();
24
+ const overlayRef = createComponent(OverlayComponent, { environmentInjector: envInjector });
25
+ appRef.attachView(overlayRef.hostView);
26
+ document.body.appendChild(overlayRef.location.nativeElement);
27
+ }),
28
+ ]
29
+ : [],
30
+ })
31
+ // eslint-disable-next-line @typescript-eslint/no-extraneous-class -- required by NgModule pattern
32
+ ], NgAnnotateModule);
33
+ export { NgAnnotateModule };
@@ -0,0 +1,51 @@
1
+ import { OnInit, ElementRef } from '@angular/core';
2
+ import type { Annotation, ComponentContext } from '../types.js';
3
+ type OverlayMode = 'hidden' | 'inspect' | 'annotate' | 'thread';
4
+ interface HighlightRect {
5
+ top: string;
6
+ left: string;
7
+ width: string;
8
+ height: string;
9
+ }
10
+ interface AnnotationBadge {
11
+ annotation: Annotation;
12
+ top: string;
13
+ left: string;
14
+ icon: string;
15
+ label: string;
16
+ }
17
+ export declare class OverlayComponent implements OnInit {
18
+ textArea?: ElementRef<HTMLTextAreaElement>;
19
+ mode: OverlayMode;
20
+ hoveredContext: ComponentContext | null;
21
+ highlightRect: HighlightRect | null;
22
+ selectedContext: ComponentContext | null;
23
+ annotationText: string;
24
+ selectionText: string;
25
+ threadAnnotation: Annotation | null;
26
+ replyText: string;
27
+ badges: AnnotationBadge[];
28
+ private readonly inspector;
29
+ private readonly bridge;
30
+ private readonly cdr;
31
+ ngOnInit(): void;
32
+ toggleInspect(event?: Event): void;
33
+ onEscape(): void;
34
+ onScrollOrResize(): void;
35
+ onMouseMove(event: MouseEvent): void;
36
+ onClick(event: MouseEvent): void;
37
+ submit(): void;
38
+ cancel(): void;
39
+ openThread(annotation: Annotation): void;
40
+ closeThread(): void;
41
+ sendReply(): void;
42
+ inputEntries(): {
43
+ key: string;
44
+ value: unknown;
45
+ }[];
46
+ private updateBadges;
47
+ private refreshBadgePositions;
48
+ private findComponentElement;
49
+ private badgeIcon;
50
+ }
51
+ export {};
@@ -0,0 +1,430 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __metadata = (this && this.__metadata) || function (k, v) {
8
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
+ };
10
+ import { Component, ChangeDetectionStrategy, ChangeDetectorRef, HostListener, ViewChild, ElementRef, inject, } from '@angular/core';
11
+ import { JsonPipe } from '@angular/common';
12
+ import { FormsModule } from '@angular/forms';
13
+ import { InspectorService } from '../inspector.service.js';
14
+ import { BridgeService } from '../bridge.service.js';
15
+ let OverlayComponent = class OverlayComponent {
16
+ constructor() {
17
+ this.mode = 'hidden';
18
+ this.hoveredContext = null;
19
+ this.highlightRect = null;
20
+ this.selectedContext = null;
21
+ this.annotationText = '';
22
+ this.selectionText = '';
23
+ this.threadAnnotation = null;
24
+ this.replyText = '';
25
+ this.badges = [];
26
+ this.inspector = inject(InspectorService);
27
+ this.bridge = inject(BridgeService);
28
+ this.cdr = inject(ChangeDetectorRef);
29
+ }
30
+ ngOnInit() {
31
+ this.bridge.annotations$.subscribe((annotations) => {
32
+ this.updateBadges(annotations);
33
+ this.cdr.markForCheck();
34
+ });
35
+ }
36
+ toggleInspect(event) {
37
+ event?.preventDefault();
38
+ if (this.mode === 'hidden')
39
+ this.mode = 'inspect';
40
+ else if (this.mode === 'inspect')
41
+ this.mode = 'hidden';
42
+ else if (this.mode === 'annotate')
43
+ this.mode = 'inspect';
44
+ this.cdr.markForCheck();
45
+ }
46
+ onEscape() {
47
+ if (this.mode === 'annotate')
48
+ this.mode = 'inspect';
49
+ else if (this.mode === 'inspect')
50
+ this.mode = 'hidden';
51
+ else if (this.mode === 'thread')
52
+ this.mode = 'hidden';
53
+ this.cdr.markForCheck();
54
+ }
55
+ onScrollOrResize() {
56
+ if (this.badges.length > 0) {
57
+ this.refreshBadgePositions();
58
+ this.cdr.markForCheck();
59
+ }
60
+ }
61
+ onMouseMove(event) {
62
+ if (this.mode !== 'inspect')
63
+ return;
64
+ const target = event.target;
65
+ if (target.closest('nga-overlay'))
66
+ return;
67
+ const context = this.inspector.getComponentContext(target);
68
+ this.hoveredContext = context;
69
+ if (context) {
70
+ const rect = target.getBoundingClientRect();
71
+ this.highlightRect = {
72
+ top: `${rect.top.toString()}px`,
73
+ left: `${rect.left.toString()}px`,
74
+ width: `${rect.width.toString()}px`,
75
+ height: `${rect.height.toString()}px`,
76
+ };
77
+ }
78
+ else {
79
+ this.highlightRect = null;
80
+ }
81
+ this.cdr.markForCheck();
82
+ }
83
+ onClick(event) {
84
+ if (this.mode !== 'inspect')
85
+ return;
86
+ const target = event.target;
87
+ if (target.closest('nga-overlay'))
88
+ return;
89
+ const context = this.inspector.getComponentContext(target);
90
+ if (!context)
91
+ return;
92
+ event.preventDefault();
93
+ event.stopPropagation();
94
+ this.selectedContext = context;
95
+ this.annotationText = '';
96
+ this.selectionText = window.getSelection()?.toString() ?? '';
97
+ this.mode = 'annotate';
98
+ this.cdr.markForCheck();
99
+ setTimeout(() => { this.textArea?.nativeElement.focus(); }, 0);
100
+ }
101
+ submit() {
102
+ if (!this.selectedContext || !this.annotationText.trim())
103
+ return;
104
+ this.bridge.createAnnotation({
105
+ ...this.selectedContext,
106
+ annotationText: this.annotationText.trim(),
107
+ selectionText: this.selectionText || undefined,
108
+ });
109
+ this.selectedContext = null;
110
+ this.annotationText = '';
111
+ this.mode = 'inspect';
112
+ this.cdr.markForCheck();
113
+ }
114
+ cancel() {
115
+ this.mode = 'inspect';
116
+ this.cdr.markForCheck();
117
+ }
118
+ openThread(annotation) {
119
+ this.threadAnnotation = annotation;
120
+ this.mode = 'thread';
121
+ this.cdr.markForCheck();
122
+ }
123
+ closeThread() {
124
+ this.threadAnnotation = null;
125
+ this.mode = 'hidden';
126
+ this.cdr.markForCheck();
127
+ }
128
+ sendReply() {
129
+ if (!this.threadAnnotation || !this.replyText.trim())
130
+ return;
131
+ this.bridge.replyToAnnotation(this.threadAnnotation.id, this.replyText.trim());
132
+ this.replyText = '';
133
+ this.cdr.markForCheck();
134
+ }
135
+ inputEntries() {
136
+ if (!this.selectedContext)
137
+ return [];
138
+ return Object.entries(this.selectedContext.inputs)
139
+ .slice(0, 5)
140
+ .map(([key, value]) => ({ key, value }));
141
+ }
142
+ updateBadges(annotations) {
143
+ this.badges = annotations
144
+ .map((annotation) => {
145
+ const el = this.findComponentElement(annotation.componentName, annotation.selector);
146
+ if (!el)
147
+ return null;
148
+ const rect = el.getBoundingClientRect();
149
+ return {
150
+ annotation,
151
+ top: `${rect.top.toString()}px`,
152
+ left: `${(rect.left + rect.width - 12).toString()}px`,
153
+ icon: this.badgeIcon(annotation.status),
154
+ label: `${annotation.componentName}: ${annotation.annotationText.slice(0, 40)}`,
155
+ };
156
+ })
157
+ .filter((b) => b !== null);
158
+ }
159
+ refreshBadgePositions() {
160
+ this.badges = this.badges.map((badge) => {
161
+ const el = this.findComponentElement(badge.annotation.componentName, badge.annotation.selector);
162
+ if (!el)
163
+ return badge;
164
+ const rect = el.getBoundingClientRect();
165
+ return {
166
+ ...badge,
167
+ top: `${rect.top.toString()}px`,
168
+ left: `${(rect.left + rect.width - 12).toString()}px`,
169
+ };
170
+ });
171
+ }
172
+ findComponentElement(componentName, selector) {
173
+ const bySelector = document.querySelector(selector);
174
+ if (bySelector)
175
+ return bySelector;
176
+ const all = document.querySelectorAll('*');
177
+ for (const el of Array.from(all)) {
178
+ try {
179
+ const comp = window
180
+ .ng;
181
+ if (comp?.getComponent(el)?.constructor.name === componentName)
182
+ return el;
183
+ }
184
+ catch {
185
+ // ignore
186
+ }
187
+ }
188
+ return null;
189
+ }
190
+ badgeIcon(status) {
191
+ const icons = {
192
+ pending: '●',
193
+ acknowledged: '◐',
194
+ resolved: '✓',
195
+ dismissed: '✕',
196
+ };
197
+ return icons[status];
198
+ }
199
+ };
200
+ __decorate([
201
+ ViewChild('textArea'),
202
+ __metadata("design:type", ElementRef)
203
+ ], OverlayComponent.prototype, "textArea", void 0);
204
+ __decorate([
205
+ HostListener('document:keydown.alt.shift.a', ['$event']),
206
+ __metadata("design:type", Function),
207
+ __metadata("design:paramtypes", [Event]),
208
+ __metadata("design:returntype", void 0)
209
+ ], OverlayComponent.prototype, "toggleInspect", null);
210
+ __decorate([
211
+ HostListener('document:keydown.escape'),
212
+ __metadata("design:type", Function),
213
+ __metadata("design:paramtypes", []),
214
+ __metadata("design:returntype", void 0)
215
+ ], OverlayComponent.prototype, "onEscape", null);
216
+ __decorate([
217
+ HostListener('window:scroll'),
218
+ HostListener('window:resize'),
219
+ __metadata("design:type", Function),
220
+ __metadata("design:paramtypes", []),
221
+ __metadata("design:returntype", void 0)
222
+ ], OverlayComponent.prototype, "onScrollOrResize", null);
223
+ __decorate([
224
+ HostListener('document:mousemove', ['$event']),
225
+ __metadata("design:type", Function),
226
+ __metadata("design:paramtypes", [MouseEvent]),
227
+ __metadata("design:returntype", void 0)
228
+ ], OverlayComponent.prototype, "onMouseMove", null);
229
+ __decorate([
230
+ HostListener('document:click', ['$event']),
231
+ __metadata("design:type", Function),
232
+ __metadata("design:paramtypes", [MouseEvent]),
233
+ __metadata("design:returntype", void 0)
234
+ ], OverlayComponent.prototype, "onClick", null);
235
+ OverlayComponent = __decorate([
236
+ Component({
237
+ selector: 'nga-overlay',
238
+ changeDetection: ChangeDetectionStrategy.OnPush,
239
+ imports: [JsonPipe, FormsModule],
240
+ styles: [`
241
+ :host {
242
+ position: fixed; top: 0; left: 0; width: 100%; height: 100%;
243
+ pointer-events: none; z-index: 9999;
244
+ }
245
+ .nga-highlight-rect {
246
+ position: fixed; border: 2px solid #3b82f6;
247
+ background: rgba(59,130,246,0.1); pointer-events: none;
248
+ transition: top 0.05s, left 0.05s, width 0.05s, height 0.05s;
249
+ }
250
+ .nga-component-label {
251
+ position: absolute; top: -22px; left: 0; background: #1e293b; color: #f8fafc;
252
+ font-family: monospace; font-size: 11px; padding: 2px 6px;
253
+ border-radius: 3px; white-space: nowrap;
254
+ }
255
+ .nga-annotate-panel, .nga-thread-panel {
256
+ pointer-events: all; position: fixed; right: 16px; top: 50%;
257
+ transform: translateY(-50%); background: #ffffff; border: 1px solid #e2e8f0;
258
+ border-radius: 8px; box-shadow: 0 4px 24px rgba(0,0,0,0.15);
259
+ padding: 16px; min-width: 320px; max-width: 400px;
260
+ }
261
+ .nga-panel-title {
262
+ margin: 0 0 12px; font-size: 14px; font-weight: 600;
263
+ color: #1e293b; font-family: monospace;
264
+ }
265
+ .nga-inputs { margin-bottom: 10px; }
266
+ .nga-input-row {
267
+ display: flex; gap: 8px; font-size: 12px;
268
+ font-family: monospace; margin-bottom: 4px;
269
+ }
270
+ .nga-input-key { color: #64748b; min-width: 80px; }
271
+ .nga-input-val {
272
+ color: #1e293b; overflow: hidden;
273
+ text-overflow: ellipsis; white-space: nowrap;
274
+ }
275
+ .nga-selection {
276
+ font-size: 12px; color: #475569; margin-bottom: 8px; font-style: italic;
277
+ }
278
+ .nga-textarea, .nga-reply-input {
279
+ width: 100%; box-sizing: border-box; border: 1px solid #cbd5e1;
280
+ border-radius: 4px; padding: 8px; font-size: 13px;
281
+ font-family: inherit; resize: vertical; margin-bottom: 10px;
282
+ }
283
+ .nga-textarea:focus, .nga-reply-input:focus {
284
+ outline: none; border-color: #3b82f6;
285
+ }
286
+ .nga-actions { display: flex; gap: 8px; }
287
+ .nga-btn {
288
+ padding: 6px 14px; border-radius: 4px; font-size: 13px;
289
+ cursor: pointer; border: none; transition: opacity 0.15s;
290
+ }
291
+ .nga-btn:disabled { opacity: 0.4; cursor: not-allowed; }
292
+ .nga-btn-submit { background: #3b82f6; color: #ffffff; }
293
+ .nga-btn-cancel { background: #f1f5f9; color: #475569; }
294
+ .nga-replies { max-height: 200px; overflow-y: auto; margin-bottom: 10px; }
295
+ .nga-reply { display: flex; gap: 8px; margin-bottom: 8px; font-size: 13px; }
296
+ .nga-reply-author { font-weight: 600; min-width: 48px; }
297
+ .nga-reply-author--agent { color: #7c3aed; }
298
+ .nga-reply-author--user { color: #2563eb; }
299
+ .nga-badge {
300
+ pointer-events: all; position: fixed; width: 18px; height: 18px;
301
+ border-radius: 50%; display: flex; align-items: center;
302
+ justify-content: center; font-size: 10px; cursor: pointer;
303
+ transform: translate(-50%, -50%);
304
+ }
305
+ .nga-badge--pending { background: #3b82f6; color: #ffffff; }
306
+ .nga-badge--acknowledged { background: #f59e0b; color: #ffffff; }
307
+ .nga-badge--resolved { background: #22c55e; color: #ffffff; }
308
+ .nga-badge--dismissed { background: #94a3b8; color: #ffffff; }
309
+ .nga-keyboard-hint {
310
+ pointer-events: none; position: fixed; bottom: 16px; right: 16px;
311
+ background: rgba(15,23,42,0.7); color: #f8fafc; font-size: 12px;
312
+ padding: 6px 10px; border-radius: 6px;
313
+ }
314
+ .nga-keyboard-hint kbd {
315
+ background: rgba(255,255,255,0.15); border-radius: 3px;
316
+ padding: 1px 5px; font-family: monospace;
317
+ }
318
+ `],
319
+ template: `
320
+ <!-- Keyboard hint -->
321
+ @if (mode === 'hidden') {
322
+ <div class="nga-keyboard-hint">
323
+ <kbd>Alt+Shift+A</kbd> to annotate
324
+ </div>
325
+ }
326
+ @if (mode === 'inspect') {
327
+ <div class="nga-keyboard-hint">
328
+ Click a component &nbsp; <kbd>Esc</kbd> to cancel
329
+ </div>
330
+ }
331
+
332
+ <!-- Inspect highlight rect -->
333
+ @if (mode === 'inspect' && hoveredContext !== null && highlightRect !== null) {
334
+ <div
335
+ class="nga-highlight-rect"
336
+ [style.top]="highlightRect.top"
337
+ [style.left]="highlightRect.left"
338
+ [style.width]="highlightRect.width"
339
+ [style.height]="highlightRect.height"
340
+ >
341
+ <span class="nga-component-label">{{ hoveredContext.componentName }}</span>
342
+ </div>
343
+ }
344
+
345
+ <!-- Annotate panel -->
346
+ @if (mode === 'annotate' && selectedContext !== null) {
347
+ <div class="nga-annotate-panel">
348
+ <h3 class="nga-panel-title">{{ selectedContext.componentName }}</h3>
349
+
350
+ @if (inputEntries().length > 0) {
351
+ <div class="nga-inputs">
352
+ @for (entry of inputEntries(); track entry.key) {
353
+ <div class="nga-input-row">
354
+ <span class="nga-input-key">{{ entry.key }}:</span>
355
+ <span class="nga-input-val">{{ entry.value | json }}</span>
356
+ </div>
357
+ }
358
+ </div>
359
+ }
360
+
361
+ @if (selectionText) {
362
+ <div class="nga-selection">
363
+ <em>"{{ selectionText }}"</em>
364
+ </div>
365
+ }
366
+
367
+ <textarea
368
+ #textArea
369
+ class="nga-textarea"
370
+ [(ngModel)]="annotationText"
371
+ placeholder="Describe the change..."
372
+ rows="4"
373
+ ></textarea>
374
+
375
+ <div class="nga-actions">
376
+ <button class="nga-btn nga-btn-submit" (click)="submit()" [disabled]="annotationText.trim() === ''">
377
+ Submit
378
+ </button>
379
+ <button class="nga-btn nga-btn-cancel" (click)="cancel()">Cancel</button>
380
+ </div>
381
+ </div>
382
+ }
383
+
384
+ <!-- Thread panel -->
385
+ @if (mode === 'thread' && threadAnnotation !== null) {
386
+ <div class="nga-thread-panel">
387
+ <h3 class="nga-panel-title">{{ threadAnnotation.componentName }}</h3>
388
+
389
+ <div class="nga-replies">
390
+ @for (reply of threadAnnotation.replies; track reply.message) {
391
+ <div class="nga-reply">
392
+ <span class="nga-reply-author nga-reply-author--{{ reply.author }}">{{ reply.author }}</span>
393
+ <span class="nga-reply-text">{{ reply.message }}</span>
394
+ </div>
395
+ }
396
+ </div>
397
+
398
+ <input
399
+ class="nga-reply-input"
400
+ type="text"
401
+ [(ngModel)]="replyText"
402
+ placeholder="Reply..."
403
+ (keydown.enter)="sendReply()"
404
+ />
405
+
406
+ <div class="nga-actions">
407
+ <button class="nga-btn nga-btn-submit" (click)="sendReply()" [disabled]="replyText.trim() === ''">
408
+ Send
409
+ </button>
410
+ <button class="nga-btn nga-btn-cancel" (click)="closeThread()">Close</button>
411
+ </div>
412
+ </div>
413
+ }
414
+
415
+ <!-- Annotation badges -->
416
+ @for (badge of badges; track badge.annotation.id) {
417
+ <div
418
+ class="nga-badge nga-badge--{{ badge.annotation.status }}"
419
+ [style.top]="badge.top"
420
+ [style.left]="badge.left"
421
+ (click)="openThread(badge.annotation)"
422
+ [title]="badge.label"
423
+ >
424
+ {{ badge.icon }}
425
+ </div>
426
+ }
427
+ `,
428
+ })
429
+ ], OverlayComponent);
430
+ export { OverlayComponent };
@@ -0,0 +1 @@
1
+ export declare function provideNgAnnotate(): import("@angular/core").EnvironmentProviders;
@@ -0,0 +1,21 @@
1
+ import { ApplicationRef, EnvironmentInjector, createComponent, inject, isDevMode, makeEnvironmentProviders, provideAppInitializer, } from '@angular/core';
2
+ import { InspectorService } from './inspector.service.js';
3
+ import { BridgeService } from './bridge.service.js';
4
+ import { OverlayComponent } from './overlay/overlay.component.js';
5
+ export function provideNgAnnotate() {
6
+ return makeEnvironmentProviders([
7
+ InspectorService,
8
+ BridgeService,
9
+ provideAppInitializer(() => {
10
+ if (!isDevMode())
11
+ return;
12
+ const bridge = inject(BridgeService);
13
+ const appRef = inject(ApplicationRef);
14
+ const envInjector = inject(EnvironmentInjector);
15
+ bridge.init();
16
+ const overlayRef = createComponent(OverlayComponent, { environmentInjector: envInjector });
17
+ appRef.attachView(overlayRef.hostView);
18
+ document.body.appendChild(overlayRef.location.nativeElement);
19
+ }),
20
+ ]);
21
+ }
@@ -0,0 +1,39 @@
1
+ export type AnnotationStatus = 'pending' | 'acknowledged' | 'resolved' | 'dismissed';
2
+ export interface AnnotationReply {
3
+ id: string;
4
+ createdAt: string;
5
+ author: 'agent' | 'user';
6
+ message: string;
7
+ }
8
+ export interface Annotation {
9
+ id: string;
10
+ sessionId: string;
11
+ createdAt: string;
12
+ status: AnnotationStatus;
13
+ replies: AnnotationReply[];
14
+ componentName: string;
15
+ componentFilePath: string;
16
+ templateFilePath?: string;
17
+ selector: string;
18
+ inputs: Record<string, unknown>;
19
+ domSnapshot: string;
20
+ componentTreePath: string[];
21
+ annotationText: string;
22
+ selectionText?: string;
23
+ }
24
+ export interface Session {
25
+ id: string;
26
+ createdAt: string;
27
+ lastSeenAt: string;
28
+ active: boolean;
29
+ url: string;
30
+ }
31
+ export interface ComponentContext {
32
+ componentName: string;
33
+ componentFilePath: string;
34
+ templateFilePath?: string;
35
+ selector: string;
36
+ inputs: Record<string, unknown>;
37
+ domSnapshot: string;
38
+ componentTreePath: string[];
39
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@ng-annotate/angular",
3
+ "version": "0.2.1",
4
+ "schematics": "./schematics/collection.json",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/yngvebn/ngagentify"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "schematics"
12
+ ],
13
+ "main": "dist/index.js",
14
+ "types": "dist/index.d.ts",
15
+ "scripts": {
16
+ "build": "tsc && npm run build:schematics",
17
+ "build:schematics": "tsc -p schematics/tsconfig.json",
18
+ "build:watch": "tsc --watch",
19
+ "lint": "eslint src/",
20
+ "lint:fix": "eslint src/ --fix"
21
+ },
22
+ "peerDependencies": {
23
+ "@angular/core": ">=21.0.0",
24
+ "rxjs": ">=7.0.0"
25
+ },
26
+ "devDependencies": {
27
+ "@angular-devkit/schematics": ">=17.0.0",
28
+ "typescript": "~5.6.0",
29
+ "@angular/core": "^21.0.0",
30
+ "rxjs": "^7.0.0"
31
+ }
32
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "$schema": "../node_modules/@angular-devkit/schematics/collection-schema.json",
3
+ "schematics": {
4
+ "ng-add": {
5
+ "description": "Add ng-annotate-mcp to the Angular project.",
6
+ "factory": "./ng-add/index"
7
+ }
8
+ }
9
+ }
@@ -0,0 +1,94 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.default = default_1;
4
+ const schematics_1 = require("@angular-devkit/schematics");
5
+ const tasks_1 = require("@angular-devkit/schematics/tasks");
6
+ function addVitePlugin() {
7
+ return (tree, context) => {
8
+ const candidates = ['vite.config.ts', 'vite.config.js', 'vite.config.mts'];
9
+ const viteConfigPath = candidates.find((p) => tree.exists(p));
10
+ if (!viteConfigPath) {
11
+ context.logger.warn('⚠️ Could not find vite.config.ts — add the plugin manually:\n' +
12
+ " import { ngAnnotateMcp } from '@ng-annotate/vite-plugin';\n" +
13
+ ' plugins: [...ngAnnotateMcp()]');
14
+ return;
15
+ }
16
+ let content = tree.read(viteConfigPath).toString('utf-8');
17
+ if (content.includes('@ng-annotate/vite-plugin')) {
18
+ context.logger.info('@ng-annotate/vite-plugin vite plugin already present, skipping.');
19
+ return;
20
+ }
21
+ // Insert import after the last existing import line
22
+ content = content.replace(/(^import .+$(\r?\n)?)+/m, (match) => match + "import { ngAnnotateMcp } from '@ng-annotate/vite-plugin';\n");
23
+ // Insert spread into plugins array (handles `plugins: [` or `plugins:[`)
24
+ content = content.replace(/plugins\s*:\s*\[/, 'plugins: [...ngAnnotateMcp(), ');
25
+ tree.overwrite(viteConfigPath, content);
26
+ context.logger.info(`✅ Added ngAnnotateMcp() to ${viteConfigPath}`);
27
+ };
28
+ }
29
+ function addProviders() {
30
+ return (tree, context) => {
31
+ const candidates = [
32
+ 'src/app/app.config.ts',
33
+ 'src/app.config.ts',
34
+ 'app/app.config.ts',
35
+ ];
36
+ const appConfigPath = candidates.find((p) => tree.exists(p));
37
+ if (!appConfigPath) {
38
+ context.logger.warn('⚠️ Could not find app.config.ts — add provideNgAnnotate() manually:\n' +
39
+ " import { provideNgAnnotate } from '@ng-annotate/angular';\n" +
40
+ ' providers: [provideNgAnnotate()]');
41
+ return;
42
+ }
43
+ let content = tree.read(appConfigPath).toString('utf-8');
44
+ if (content.includes('provideNgAnnotate')) {
45
+ context.logger.info('provideNgAnnotate already present, skipping.');
46
+ return;
47
+ }
48
+ // Insert import after the last existing import line
49
+ content = content.replace(/(^import .+$(\r?\n)?)+/m, (match) => match + "import { provideNgAnnotate } from '@ng-annotate/angular';\n");
50
+ // Insert into providers array
51
+ content = content.replace(/providers\s*:\s*\[/, 'providers: [\n provideNgAnnotate(),');
52
+ tree.overwrite(appConfigPath, content);
53
+ context.logger.info(`✅ Added provideNgAnnotate() to ${appConfigPath}`);
54
+ };
55
+ }
56
+ function addMcpConfig() {
57
+ return (tree, context) => {
58
+ if (tree.exists('.mcp.json')) {
59
+ context.logger.info('.mcp.json already exists, skipping.');
60
+ return;
61
+ }
62
+ const mcpConfig = {
63
+ mcpServers: {
64
+ 'ng-annotate': {
65
+ command: 'npx',
66
+ args: ['-y', '@ng-annotate/mcp-server'],
67
+ },
68
+ },
69
+ };
70
+ tree.create('.mcp.json', JSON.stringify(mcpConfig, null, 2) + '\n');
71
+ context.logger.info('✅ Created .mcp.json');
72
+ };
73
+ }
74
+ function addDevDependency() {
75
+ return (tree, context) => {
76
+ const pkgPath = 'package.json';
77
+ if (!tree.exists(pkgPath))
78
+ return;
79
+ const pkg = JSON.parse(tree.read(pkgPath).toString('utf-8'));
80
+ pkg['devDependencies'] ?? (pkg['devDependencies'] = {});
81
+ if (!pkg['devDependencies']['@ng-annotate/vite-plugin']) {
82
+ pkg['devDependencies']['@ng-annotate/vite-plugin'] = 'latest';
83
+ tree.overwrite(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
84
+ context.addTask(new tasks_1.NodePackageInstallTask());
85
+ context.logger.info('✅ Added @ng-annotate/vite-plugin to devDependencies');
86
+ }
87
+ };
88
+ }
89
+ function default_1() {
90
+ return (tree, context) => {
91
+ context.logger.info('Setting up @ng-annotate/vite-plugin...');
92
+ return (0, schematics_1.chain)([addDevDependency(), addVitePlugin(), addProviders(), addMcpConfig()])(tree, context);
93
+ };
94
+ }
@@ -0,0 +1,127 @@
1
+ import { Rule, SchematicContext, Tree, chain } from '@angular-devkit/schematics';
2
+ import { NodePackageInstallTask } from '@angular-devkit/schematics/tasks';
3
+
4
+ function addVitePlugin(): Rule {
5
+ return (tree: Tree, context: SchematicContext) => {
6
+ const candidates = ['vite.config.ts', 'vite.config.js', 'vite.config.mts'];
7
+ const viteConfigPath = candidates.find((p) => tree.exists(p));
8
+
9
+ if (!viteConfigPath) {
10
+ context.logger.warn(
11
+ '⚠️ Could not find vite.config.ts — add the plugin manually:\n' +
12
+ " import { ngAnnotateMcp } from '@ng-annotate/vite-plugin';\n" +
13
+ ' plugins: [...ngAnnotateMcp()]',
14
+ );
15
+ return;
16
+ }
17
+
18
+ let content = tree.read(viteConfigPath)!.toString('utf-8');
19
+
20
+ if (content.includes('@ng-annotate/vite-plugin')) {
21
+ context.logger.info('@ng-annotate/vite-plugin vite plugin already present, skipping.');
22
+ return;
23
+ }
24
+
25
+ // Insert import after the last existing import line
26
+ content = content.replace(
27
+ /(^import .+$(\r?\n)?)+/m,
28
+ (match) => match + "import { ngAnnotateMcp } from '@ng-annotate/vite-plugin';\n",
29
+ );
30
+
31
+ // Insert spread into plugins array (handles `plugins: [` or `plugins:[`)
32
+ content = content.replace(/plugins\s*:\s*\[/, 'plugins: [...ngAnnotateMcp(), ');
33
+
34
+ tree.overwrite(viteConfigPath, content);
35
+ context.logger.info(`✅ Added ngAnnotateMcp() to ${viteConfigPath}`);
36
+ };
37
+ }
38
+
39
+ function addProviders(): Rule {
40
+ return (tree: Tree, context: SchematicContext) => {
41
+ const candidates = [
42
+ 'src/app/app.config.ts',
43
+ 'src/app.config.ts',
44
+ 'app/app.config.ts',
45
+ ];
46
+ const appConfigPath = candidates.find((p) => tree.exists(p));
47
+
48
+ if (!appConfigPath) {
49
+ context.logger.warn(
50
+ '⚠️ Could not find app.config.ts — add provideNgAnnotate() manually:\n' +
51
+ " import { provideNgAnnotate } from '@ng-annotate/angular';\n" +
52
+ ' providers: [provideNgAnnotate()]',
53
+ );
54
+ return;
55
+ }
56
+
57
+ let content = tree.read(appConfigPath)!.toString('utf-8');
58
+
59
+ if (content.includes('provideNgAnnotate')) {
60
+ context.logger.info('provideNgAnnotate already present, skipping.');
61
+ return;
62
+ }
63
+
64
+ // Insert import after the last existing import line
65
+ content = content.replace(
66
+ /(^import .+$(\r?\n)?)+/m,
67
+ (match) => match + "import { provideNgAnnotate } from '@ng-annotate/angular';\n",
68
+ );
69
+
70
+ // Insert into providers array
71
+ content = content.replace(/providers\s*:\s*\[/, 'providers: [\n provideNgAnnotate(),');
72
+
73
+ tree.overwrite(appConfigPath, content);
74
+ context.logger.info(`✅ Added provideNgAnnotate() to ${appConfigPath}`);
75
+ };
76
+ }
77
+
78
+ function addMcpConfig(): Rule {
79
+ return (tree: Tree, context: SchematicContext) => {
80
+ if (tree.exists('.mcp.json')) {
81
+ context.logger.info('.mcp.json already exists, skipping.');
82
+ return;
83
+ }
84
+
85
+ const mcpConfig = {
86
+ mcpServers: {
87
+ 'ng-annotate': {
88
+ command: 'npx',
89
+ args: ['-y', '@ng-annotate/mcp-server'],
90
+ },
91
+ },
92
+ };
93
+
94
+ tree.create('.mcp.json', JSON.stringify(mcpConfig, null, 2) + '\n');
95
+ context.logger.info('✅ Created .mcp.json');
96
+ };
97
+ }
98
+
99
+ function addDevDependency(): Rule {
100
+ return (tree: Tree, context: SchematicContext) => {
101
+ const pkgPath = 'package.json';
102
+ if (!tree.exists(pkgPath)) return;
103
+
104
+ const pkg = JSON.parse(tree.read(pkgPath)!.toString('utf-8')) as Record<
105
+ string,
106
+ Record<string, string>
107
+ >;
108
+ pkg['devDependencies'] ??= {};
109
+
110
+ if (!pkg['devDependencies']['@ng-annotate/vite-plugin']) {
111
+ pkg['devDependencies']['@ng-annotate/vite-plugin'] = 'latest';
112
+ tree.overwrite(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
113
+ context.addTask(new NodePackageInstallTask());
114
+ context.logger.info('✅ Added @ng-annotate/vite-plugin to devDependencies');
115
+ }
116
+ };
117
+ }
118
+
119
+ export default function (): Rule {
120
+ return (tree: Tree, context: SchematicContext) => {
121
+ context.logger.info('Setting up @ng-annotate/vite-plugin...');
122
+ return chain([addDevDependency(), addVitePlugin(), addProviders(), addMcpConfig()])(
123
+ tree,
124
+ context,
125
+ );
126
+ };
127
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "module": "CommonJS",
4
+ "moduleResolution": "node",
5
+ "target": "ES2020",
6
+ "lib": ["ES2020"],
7
+ "declaration": false,
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "rootDir": ".",
12
+ "outDir": "."
13
+ },
14
+ "include": ["./**/*.ts"],
15
+ "exclude": ["**/*.spec.ts", "node_modules"]
16
+ }