import * as i0 from '@angular/core';
import { signal, Injectable, InjectionToken, inject, DOCUMENT, PLATFORM_ID, isDevMode, Injector, runInInjectionContext, afterEveryRender, ChangeDetectionStrategy, Component, makeEnvironmentProviders, provideEnvironmentInitializer, EnvironmentInjector, ApplicationRef, createComponent } from '@angular/core';
import { isPlatformBrowser, DOCUMENT as DOCUMENT$1 } from '@angular/common';

class ComponentTracker {
    // WeakMap: component instance → stats. GC-safe: entries are freed when
    // the component instance is garbage collected after destruction.
    byInstance = new WeakMap();
    // Strong map: host element → component instance. Used for mutation-to-component
    // lookup. Entries must be manually removed when components are destroyed.
    byElement = new Map();
    _totalRenders = signal(0, ...(ngDevMode ? [{ debugName: "_totalRenders" }] : []));
    _totalUnnecessary = signal(0, ...(ngDevMode ? [{ debugName: "_totalUnnecessary" }] : []));
    _trackedComponents = signal([], ...(ngDevMode ? [{ debugName: "_trackedComponents" }] : []));
    totalRenders = this._totalRenders.asReadonly();
    totalUnnecessary = this._totalUnnecessary.asReadonly();
    trackedComponents = this._trackedComponents.asReadonly();
    /**
     * Record a render event for a component.
     * NOTE: Must be called outside of a profiler callback to avoid triggering CD.
     * Use queueMicrotask() to defer calls from profiler events.
     */
    recordRender(instance, hostElement, kind) {
        let stats = this.byInstance.get(instance);
        if (!stats) {
            stats = {
                componentName: instance.constructor.name,
                hostElement,
                totalRenders: 0,
                unnecessaryRenders: 0,
                lastRenderKind: kind,
                lastRenderTimestamp: 0,
            };
            this.byInstance.set(instance, stats);
            this.byElement.set(hostElement, instance);
        }
        stats.totalRenders++;
        stats.lastRenderKind = kind;
        stats.lastRenderTimestamp = Date.now();
        if (kind === 'unnecessary') {
            stats.unnecessaryRenders++;
            this._totalUnnecessary.update(n => n + 1);
        }
        this._totalRenders.update(n => n + 1);
        return stats;
    }
    /**
     * Rebuild the trackedComponents signal from the current element map.
     * Call once per tick after all recordRender() calls.
     */
    snapshotTrackedComponents() {
        const list = [];
        for (const [, instance] of this.byElement) {
            const stats = this.byInstance.get(instance);
            if (stats)
                list.push(stats);
        }
        this._trackedComponents.set(list);
    }
    /** Remove a component from tracking (call when its host element is removed). */
    unregister(el) {
        this.byElement.delete(el);
    }
    reset() {
        this.byElement.clear();
        this._totalRenders.set(0);
        this._totalUnnecessary.set(0);
        this._trackedComponents.set([]);
    }
    static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.1", ngImport: i0, type: ComponentTracker, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
    static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.1", ngImport: i0, type: ComponentTracker, providedIn: 'root' });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.1", ngImport: i0, type: ComponentTracker, decorators: [{
            type: Injectable,
            args: [{ providedIn: 'root' }]
        }] });

// rgb(...) strings for fill and stroke
const RENDER_COLOR = '255, 200, 0'; // yellow — normal re-render
const UNNECESSARY_COLOR = '255, 60, 60'; // red — unnecessary render
class CanvasOverlay {
    canvas = null;
    ctx = null;
    rects = [];
    rafId = 0;
    attached = false;
    attach(doc) {
        if (this.attached)
            return;
        const canvas = doc.createElement('canvas');
        canvas.setAttribute('aria-hidden', 'true');
        canvas.setAttribute('role', 'presentation');
        canvas.style.cssText = [
            'position:fixed',
            'top:0',
            'left:0',
            'width:100%',
            'height:100%',
            'pointer-events:none',
            `z-index:2147483646`,
        ].join(';');
        doc.body.appendChild(canvas);
        this.canvas = canvas;
        this.ctx = canvas.getContext('2d');
        this.attached = true;
        this.resize();
        window.addEventListener('resize', this.onResize);
        this.scheduleFrame();
    }
    detach() {
        if (!this.attached)
            return;
        this.attached = false;
        cancelAnimationFrame(this.rafId);
        window.removeEventListener('resize', this.onResize);
        this.canvas?.remove();
        this.canvas = null;
        this.ctx = null;
        this.rects = [];
    }
    /** Queue a flash animation over an element's bounding rect. */
    flash(element, kind, durationMs) {
        if (!this.attached)
            return;
        const rect = element.getBoundingClientRect();
        if (rect.width === 0 || rect.height === 0)
            return;
        this.rects.push({
            x: rect.left,
            y: rect.top,
            width: rect.width,
            height: rect.height,
            kind,
            startTime: performance.now(),
            durationMs,
        });
    }
    onResize = () => this.resize();
    resize() {
        if (!this.canvas)
            return;
        this.canvas.width = window.innerWidth;
        this.canvas.height = window.innerHeight;
    }
    scheduleFrame() {
        if (!this.attached)
            return;
        this.rafId = requestAnimationFrame(this.drawFrame);
    }
    drawFrame = (now) => {
        if (!this.ctx || !this.canvas || !this.attached)
            return;
        this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
        // Decay alphas and remove expired rects
        this.rects = this.rects.filter(r => {
            const elapsed = now - r.startTime;
            return elapsed < r.durationMs;
        });
        for (const r of this.rects) {
            const elapsed = now - r.startTime;
            const alpha = Math.max(0, 1 - elapsed / r.durationMs);
            const color = r.kind === 'render' ? RENDER_COLOR : UNNECESSARY_COLOR;
            // Semi-transparent fill
            this.ctx.fillStyle = `rgba(${color}, ${alpha * 0.1})`;
            this.ctx.fillRect(r.x, r.y, r.width, r.height);
            // Solid border
            this.ctx.strokeStyle = `rgba(${color}, ${alpha * 0.9})`;
            this.ctx.lineWidth = 2;
            this.ctx.strokeRect(r.x + 1, r.y + 1, r.width - 2, r.height - 2);
        }
        this.scheduleFrame();
    };
}

const ANGULAR_SCAN_OPTIONS = new InjectionToken('ANGULAR_SCAN_OPTIONS', { providedIn: 'root', factory: () => ({}) });

class ScanConfigService {
    options = inject(ANGULAR_SCAN_OPTIONS);
    enabled = signal(this.options.enabled !== false, ...(ngDevMode ? [{ debugName: "enabled" }] : []));
    showOverlay = signal(true, ...(ngDevMode ? [{ debugName: "showOverlay" }] : []));
    showBadges = signal(this.options.showBadges !== false, ...(ngDevMode ? [{ debugName: "showBadges" }] : []));
    flashDurationMs = signal(this.options.flashDurationMs ?? 500, ...(ngDevMode ? [{ debugName: "flashDurationMs" }] : []));
    static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.1", ngImport: i0, type: ScanConfigService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
    static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.1", ngImport: i0, type: ScanConfigService, providedIn: 'root' });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.1", ngImport: i0, type: ScanConfigService, decorators: [{
            type: Injectable,
            args: [{ providedIn: 'root' }]
        }] });

class OverlayService {
    document = inject(DOCUMENT);
    platformId = inject(PLATFORM_ID);
    options = inject(ANGULAR_SCAN_OPTIONS);
    config = inject(ScanConfigService);
    canvas = new CanvasOverlay();
    // Badge elements keyed by host element
    badges = new Map();
    initialize() {
        if (!isDevMode())
            return;
        if (!isPlatformBrowser(this.platformId))
            return;
        if (this.options.enabled === false)
            return;
        this.canvas.attach(this.document);
    }
    destroy() {
        this.canvas.detach();
        for (const badge of this.badges.values())
            badge.remove();
        this.badges.clear();
    }
    onComponentChecked(stats) {
        if (this.config.showOverlay()) {
            this.canvas.flash(stats.hostElement, stats.lastRenderKind, this.config.flashDurationMs());
        }
        if (this.config.showBadges()) {
            this.updateBadge(stats);
        }
        else {
            this.removeBadge(stats.hostElement);
        }
    }
    removeBadge(el) {
        this.badges.get(el)?.remove();
        this.badges.delete(el);
    }
    updateBadge(stats) {
        let badge = this.badges.get(stats.hostElement);
        if (!badge) {
            badge = this.document.createElement('div');
            badge.setAttribute('aria-hidden', 'true');
            badge.setAttribute('role', 'presentation');
            badge.style.cssText = [
                'position:absolute',
                'top:2px',
                'right:2px',
                'z-index:2147483645',
                'pointer-events:none',
                'font:bold 9px/13px monospace',
                'padding:1px 4px',
                'border-radius:3px',
                'min-width:16px',
                'text-align:center',
                'color:#fff',
                'white-space:nowrap',
            ].join(';');
            // Host element must be non-static to contain an absolutely-positioned badge
            this.ensurePositioned(stats.hostElement);
            stats.hostElement.appendChild(badge);
            this.badges.set(stats.hostElement, badge);
        }
        const isUnnecessary = stats.lastRenderKind === 'unnecessary';
        badge.style.background = isUnnecessary ? '#f44336' : '#ff9800';
        badge.textContent = String(stats.totalRenders);
        badge.title = `${stats.componentName}: ${stats.totalRenders} renders, ${stats.unnecessaryRenders} unnecessary`;
    }
    ensurePositioned(el) {
        const htmlEl = el;
        if (getComputedStyle(htmlEl).position === 'static') {
            htmlEl.style.position = 'relative';
        }
    }
    static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.1", ngImport: i0, type: OverlayService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
    static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.1", ngImport: i0, type: OverlayService, providedIn: 'root' });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.1", ngImport: i0, type: OverlayService, decorators: [{
            type: Injectable,
            args: [{ providedIn: 'root' }]
        }] });

/**
 * Returns the Angular debug API (window.ng) if available.
 * Returns null in SSR, production builds, or when Angular DevTools APIs are absent.
 *
 * Safe to call before Angular is bootstrapped — the API is attached to window
 * by Angular's dev mode runtime.
 */
function getNgDebugApi() {
    if (typeof window === 'undefined')
        return null;
    const ng = window['ng'];
    if (!ng || typeof ng !== 'object')
        return null;
    const api = ng;
    // ɵsetProfiler is the sentinel for dev mode — absent in production builds
    if (typeof api.ɵsetProfiler !== 'function')
        return null;
    if (typeof api.getHostElement !== 'function')
        return null;
    return api;
}

// Angular profiler event IDs (stable contract in dev mode, used by Angular DevTools)
const TemplateUpdateStart$1 = 2;
const ChangeDetectionStart$1 = 12;
const ChangeDetectionEnd$1 = 13;
const ChangeDetectionSyncStart$1 = 14;
const ChangeDetectionSyncEnd$1 = 15;
class ScannerService {
    tracker = inject(ComponentTracker);
    overlay = inject(OverlayService);
    options = inject(ANGULAR_SCAN_OPTIONS);
    platformId = inject(PLATFORM_ID);
    injector = inject(Injector);
    config = inject(ScanConfigService);
    removeProfiler = null;
    afterRenderRef = null;
    mutationObserver = null;
    // Accumulated during a single CD tick (raw DOM nodes, no signal writes)
    mutatedNodes = new Set();
    // Component instances visited during the sync (template update) phase
    tickInstances = new Set();
    // Updates waiting to be flushed in afterEveryRender
    pendingFlush = [];
    // Tracks which phase of CD we're in
    inSyncPhase = false;
    inTick = false;
    // The toolbar component instance — excluded from tracking to avoid self-loops
    toolbarInstance = null;
    initialize() {
        if (!isDevMode())
            return;
        if (!isPlatformBrowser(this.platformId))
            return;
        if (this.options.enabled === false)
            return;
        const ng = getNgDebugApi();
        if (!ng) {
            console.warn('[angular-scan] Angular debug APIs (window.ng) not available. ' +
                'Ensure you are running in development mode.');
            return;
        }
        // afterEveryRender is Angular's safe hook for post-render writes.
        // Signal writes here cause exactly one extra toolbar render pass then stabilize,
        // avoiding the infinite loop that queueMicrotask causes in zone.js + signals hybrid apps.
        this.afterRenderRef = runInInjectionContext(this.injector, () => afterEveryRender({ write: () => this.flushPendingUpdates() }));
        this.mutationObserver = new MutationObserver(records => {
            for (const r of records) {
                this.mutatedNodes.add(r.target);
                r.addedNodes.forEach(n => this.mutatedNodes.add(n));
                r.removedNodes.forEach(n => this.mutatedNodes.add(n));
            }
        });
        this.removeProfiler = ng.ɵsetProfiler((event, instance) => {
            switch (event) {
                case ChangeDetectionStart$1:
                    this.onTickStart();
                    break;
                case ChangeDetectionSyncStart$1:
                    this.inSyncPhase = true;
                    break;
                case ChangeDetectionSyncEnd$1:
                    this.inSyncPhase = false;
                    break;
                case TemplateUpdateStart$1:
                    // Only record during the real update phase, not the dev-mode checkNoChanges pass
                    if (this.config.enabled() && this.inSyncPhase && instance && instance !== this.toolbarInstance) {
                        this.tickInstances.add(instance);
                    }
                    break;
                case ChangeDetectionEnd$1:
                    this.onTickEnd(ng);
                    break;
            }
        });
    }
    /** Exclude a component instance from render tracking (used for the toolbar). */
    setToolbarInstance(instance) {
        this.toolbarInstance = instance;
    }
    onTickStart() {
        if (this.inTick)
            return;
        this.inTick = true;
        this.mutatedNodes.clear();
        this.tickInstances.clear();
        this.mutationObserver?.observe(document.body, {
            subtree: true,
            childList: true,
            attributes: true,
            characterData: true,
        });
    }
    onTickEnd(ng) {
        if (!this.inTick)
            return;
        this.inTick = false;
        this.inSyncPhase = false;
        // Synchronously flush pending mutation records before disconnecting
        const pending = this.mutationObserver?.takeRecords() ?? [];
        for (const r of pending) {
            this.mutatedNodes.add(r.target);
            r.addedNodes.forEach(n => this.mutatedNodes.add(n));
            r.removedNodes.forEach(n => this.mutatedNodes.add(n));
        }
        this.mutationObserver?.disconnect();
        // Build a set of component host elements that had DOM mutations
        // Walk mutated nodes UP to find their owning component host element
        const mutatedHosts = this.buildMutatedHosts(ng);
        // Collect updates as pure data — NO signal writes here (would trigger CD)
        const pendingUpdates = [];
        for (const instance of this.tickInstances) {
            try {
                const hostElement = ng.getHostElement(instance);
                if (!hostElement)
                    continue;
                const hadMutation = mutatedHosts.has(hostElement);
                const kind = hadMutation ? 'render' : 'unnecessary';
                pendingUpdates.push({ instance, hostElement, kind });
            }
            catch {
                // Component may have been destroyed during the tick
            }
        }
        // Queue for afterEveryRender to flush safely — no direct signal writes here.
        this.pendingFlush.push(...pendingUpdates);
    }
    /**
     * Called by afterEveryRender. Drains pendingFlush into signals safely.
     * If there are no pending updates, returns immediately so Angular stabilizes.
     * If there are updates, signals are written → toolbar re-renders (one extra pass) → stabilizes.
     */
    flushPendingUpdates() {
        const updates = this.pendingFlush.splice(0);
        if (updates.length === 0)
            return;
        for (const update of updates) {
            const stats = this.tracker.recordRender(update.instance, update.hostElement, update.kind);
            this.overlay.onComponentChecked(stats);
        }
        this.tracker.snapshotTrackedComponents();
    }
    /**
     * Build a Set of host Elements by walking each mutated node up the DOM tree
     * until an Angular component boundary is found.
     * This is O(mutatedNodes × depth) but depth is typically < 20.
     */
    buildMutatedHosts(ng) {
        const hosts = new Set();
        for (const node of this.mutatedNodes) {
            let current = node;
            while (current && current !== document.body) {
                if (current instanceof Element) {
                    try {
                        const component = ng.getComponent(current);
                        if (component) {
                            hosts.add(current);
                            break;
                        }
                    }
                    catch {
                        // ignore
                    }
                }
                current = current.parentNode;
            }
        }
        return hosts;
    }
    ngOnDestroy() {
        this.removeProfiler?.();
        this.removeProfiler = null;
        this.afterRenderRef?.destroy();
        this.afterRenderRef = null;
        this.mutationObserver?.disconnect();
        this.mutationObserver = null;
        this.pendingFlush = [];
        this.overlay.destroy();
    }
    static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.1", ngImport: i0, type: ScannerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
    static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.1", ngImport: i0, type: ScannerService, providedIn: 'root' });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.1", ngImport: i0, type: ScannerService, decorators: [{
            type: Injectable,
            args: [{ providedIn: 'root' }]
        }] });

class ToolbarComponent {
    tracker = inject(ComponentTracker);
    config = inject(ScanConfigService);
    expanded = signal(false, ...(ngDevMode ? [{ debugName: "expanded" }] : []));
    settingsOpen = signal(false, ...(ngDevMode ? [{ debugName: "settingsOpen" }] : []));
    toggleExpanded() {
        this.expanded.set(!this.expanded());
        if (this.expanded())
            this.settingsOpen.set(false);
    }
    toggleSettings() {
        this.settingsOpen.set(!this.settingsOpen());
        if (this.settingsOpen())
            this.expanded.set(false);
    }
    toggleEnabled() {
        this.config.enabled.set(!this.config.enabled());
    }
    toggleOverlay() {
        this.config.showOverlay.set(!this.config.showOverlay());
    }
    toggleBadges() {
        this.config.showBadges.set(!this.config.showBadges());
    }
    resetStats() {
        this.tracker.reset();
    }
    onFlashDurationChange(event) {
        const value = Number(event.target.value);
        this.config.flashDurationMs.set(value);
    }
    static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.1", ngImport: i0, type: ToolbarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
    static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.1", type: ToolbarComponent, isStandalone: true, selector: "angular-scan-toolbar", host: { properties: { "style.display": "\"block\"" } }, ngImport: i0, template: "<div\n  class=\"toolbar\"\n  role=\"complementary\"\n  aria-label=\"Angular Scan DevTools\"\n>\n  <div class=\"toolbar__header\">\n    <span class=\"toolbar__logo\" aria-hidden=\"true\">\u25C9</span>\n    <span class=\"toolbar__title\">angular-scan</span>\n\n    <button\n      class=\"toolbar__btn\"\n      type=\"button\"\n      [attr.aria-pressed]=\"!config.enabled()\"\n      [title]=\"config.enabled() ? 'Pause scanning' : 'Resume scanning'\"\n      (click)=\"toggleEnabled()\"\n    >{{ config.enabled() ? '\u23F8' : '\u25B6' }}</button>\n\n    <button\n      class=\"toolbar__btn\"\n      type=\"button\"\n      [attr.aria-pressed]=\"settingsOpen()\"\n      title=\"Settings\"\n      (click)=\"toggleSettings()\"\n    >\u2699</button>\n\n    <button\n      class=\"toolbar__btn\"\n      type=\"button\"\n      [attr.aria-pressed]=\"expanded()\"\n      [title]=\"expanded() ? 'Collapse inspector' : 'Expand inspector'\"\n      (click)=\"toggleExpanded()\"\n    >{{ expanded() ? '\u25B2' : '\u25BC' }}</button>\n  </div>\n\n  <div class=\"toolbar__stats\">\n    <span class=\"toolbar__stat\" title=\"Total change detection checks\">\n      <span class=\"toolbar__stat-label\">checks</span>\n      <strong class=\"toolbar__stat-value\">{{ tracker.totalRenders() }}</strong>\n    </span>\n    <span\n      class=\"toolbar__stat\"\n      [class.toolbar__stat--warn]=\"tracker.totalUnnecessary() > 0\"\n      title=\"Unnecessary renders: component was checked but DOM did not change\"\n    >\n      <span class=\"toolbar__stat-label\">wasted</span>\n      <strong class=\"toolbar__stat-value\">{{ tracker.totalUnnecessary() }}</strong>\n    </span>\n  </div>\n\n  @if (settingsOpen()) {\n    <div class=\"toolbar__settings\" role=\"group\" aria-label=\"Scan settings\">\n\n      <label class=\"toolbar__setting\">\n        <span class=\"toolbar__setting-label\">Enable scanning</span>\n        <button\n          class=\"toolbar__toggle\"\n          type=\"button\"\n          role=\"switch\"\n          [attr.aria-checked]=\"config.enabled()\"\n          (click)=\"toggleEnabled()\"\n          [class.toolbar__toggle--on]=\"config.enabled()\"\n        >{{ config.enabled() ? 'ON' : 'OFF' }}</button>\n      </label>\n\n      <label class=\"toolbar__setting\">\n        <span class=\"toolbar__setting-label\">Flash overlay</span>\n        <button\n          class=\"toolbar__toggle\"\n          type=\"button\"\n          role=\"switch\"\n          [attr.aria-checked]=\"config.showOverlay()\"\n          (click)=\"toggleOverlay()\"\n          [class.toolbar__toggle--on]=\"config.showOverlay()\"\n        >{{ config.showOverlay() ? 'ON' : 'OFF' }}</button>\n      </label>\n\n      <label class=\"toolbar__setting\">\n        <span class=\"toolbar__setting-label\">Render badges</span>\n        <button\n          class=\"toolbar__toggle\"\n          type=\"button\"\n          role=\"switch\"\n          [attr.aria-checked]=\"config.showBadges()\"\n          (click)=\"toggleBadges()\"\n          [class.toolbar__toggle--on]=\"config.showBadges()\"\n        >{{ config.showBadges() ? 'ON' : 'OFF' }}</button>\n      </label>\n\n      <label class=\"toolbar__setting toolbar__setting--column\">\n        <span class=\"toolbar__setting-label\">\n          Flash duration\n          <span class=\"toolbar__setting-value\">{{ config.flashDurationMs() }}ms</span>\n        </span>\n        <input\n          class=\"toolbar__slider\"\n          type=\"range\"\n          min=\"100\"\n          max=\"2000\"\n          step=\"100\"\n          [value]=\"config.flashDurationMs()\"\n          (input)=\"onFlashDurationChange($event)\"\n          aria-label=\"Flash duration in milliseconds\"\n        />\n      </label>\n\n      <button\n        class=\"toolbar__reset\"\n        type=\"button\"\n        title=\"Clear all render stats\"\n        (click)=\"resetStats()\"\n      >\u21BA Reset stats</button>\n\n    </div>\n  }\n\n  @if (expanded()) {\n    <div\n      class=\"toolbar__inspector\"\n      role=\"list\"\n      aria-label=\"Component render counts\"\n    >\n      @for (comp of tracker.trackedComponents(); track comp.hostElement) {\n        <div\n          class=\"toolbar__component\"\n          role=\"listitem\"\n          [class.toolbar__component--warn]=\"comp.lastRenderKind === 'unnecessary'\"\n          [title]=\"comp.componentName + ' \u2014 ' + comp.totalRenders + ' checks, ' + comp.unnecessaryRenders + ' wasted'\"\n        >\n          <span class=\"toolbar__component-name\">{{ comp.componentName }}</span>\n          <span class=\"toolbar__component-count\">{{ comp.totalRenders }}</span>\n          @if (comp.unnecessaryRenders > 0) {\n            <span class=\"toolbar__component-wasted\" aria-label=\"wasted renders\">\n              {{ comp.unnecessaryRenders }}W\n            </span>\n          }\n        </div>\n      }\n    </div>\n  }\n</div>\n", styles: [":host{position:fixed;bottom:0;right:0;z-index:2147483647;font-family:ui-monospace,Cascadia Code,Source Code Pro,Menlo,Consolas,monospace;font-size:11px;line-height:1.4}.toolbar{background:#0f1117;color:#c9d1d9;border:1px solid #30363d;border-bottom:none;border-right:none;border-radius:6px 0 0;min-width:200px;max-width:300px;box-shadow:-2px -2px 12px #00000080}.toolbar__header{display:flex;align-items:center;gap:4px;padding:5px 8px;border-bottom:1px solid #21262d}.toolbar__logo{color:#58a6ff;font-size:12px}.toolbar__title{font-weight:600;color:#58a6ff;flex:1;letter-spacing:.02em}.toolbar__btn{background:transparent;border:1px solid #30363d;color:#8b949e;border-radius:4px;cursor:pointer;padding:2px 6px;font-size:10px;transition:color .15s,border-color .15s}.toolbar__btn:hover{color:#c9d1d9;border-color:#58a6ff}.toolbar__btn:focus-visible{outline:2px solid #58a6ff;outline-offset:2px}.toolbar__stats{display:flex;gap:12px;padding:5px 8px}.toolbar__stat{display:flex;flex-direction:column;gap:1px}.toolbar__stat-label{color:#8b949e;font-size:9px;text-transform:uppercase;letter-spacing:.05em}.toolbar__stat-value{color:#c9d1d9}.toolbar__stat--warn .toolbar__stat-value{color:#f85149}.toolbar__inspector{max-height:220px;overflow-y:auto;border-top:1px solid #21262d}.toolbar__inspector::-webkit-scrollbar{width:4px}.toolbar__inspector::-webkit-scrollbar-track{background:transparent}.toolbar__inspector::-webkit-scrollbar-thumb{background:#30363d;border-radius:2px}.toolbar__component{display:flex;align-items:center;gap:6px;padding:3px 8px;border-bottom:1px solid #161b22;transition:background .1s}.toolbar__component:hover{background:#161b22}.toolbar__component--warn{background:#f851490f}.toolbar__component-name{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#8b949e}.toolbar__component-count{font-weight:600;color:#ffa657;min-width:20px;text-align:right}.toolbar__component-wasted{color:#f85149;font-size:9px;font-weight:600}.toolbar__settings{display:flex;flex-direction:column;gap:6px;padding:8px;border-top:1px solid #21262d}.toolbar__setting{display:flex;align-items:center;justify-content:space-between;gap:8px;cursor:default}.toolbar__setting--column{flex-direction:column;align-items:stretch;gap:4px}.toolbar__setting-label{color:#8b949e;font-size:10px;display:flex;align-items:center;justify-content:space-between;gap:4px}.toolbar__setting-value{color:#58a6ff;font-weight:600}.toolbar__toggle{background:#21262d;border:1px solid #30363d;color:#8b949e;border-radius:3px;cursor:pointer;padding:1px 6px;font-size:9px;font-weight:600;font-family:inherit;letter-spacing:.05em;min-width:32px;transition:background .15s,color .15s,border-color .15s}.toolbar__toggle--on{background:#58a6ff26;border-color:#58a6ff;color:#58a6ff}.toolbar__toggle:focus-visible{outline:2px solid #58a6ff;outline-offset:2px}.toolbar__slider{width:100%;accent-color:#58a6ff;cursor:pointer;height:4px}.toolbar__reset{background:transparent;border:1px solid #30363d;color:#8b949e;border-radius:4px;cursor:pointer;padding:3px 8px;font-size:10px;font-family:inherit;margin-top:2px;transition:color .15s,border-color .15s;align-self:flex-start}.toolbar__reset:hover{color:#f85149;border-color:#f85149}.toolbar__reset:focus-visible{outline:2px solid #58a6ff;outline-offset:2px}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.1", ngImport: i0, type: ToolbarComponent, decorators: [{
            type: Component,
            args: [{ selector: 'angular-scan-toolbar', changeDetection: ChangeDetectionStrategy.OnPush, host: { '[style.display]': '"block"' }, template: "<div\n  class=\"toolbar\"\n  role=\"complementary\"\n  aria-label=\"Angular Scan DevTools\"\n>\n  <div class=\"toolbar__header\">\n    <span class=\"toolbar__logo\" aria-hidden=\"true\">\u25C9</span>\n    <span class=\"toolbar__title\">angular-scan</span>\n\n    <button\n      class=\"toolbar__btn\"\n      type=\"button\"\n      [attr.aria-pressed]=\"!config.enabled()\"\n      [title]=\"config.enabled() ? 'Pause scanning' : 'Resume scanning'\"\n      (click)=\"toggleEnabled()\"\n    >{{ config.enabled() ? '\u23F8' : '\u25B6' }}</button>\n\n    <button\n      class=\"toolbar__btn\"\n      type=\"button\"\n      [attr.aria-pressed]=\"settingsOpen()\"\n      title=\"Settings\"\n      (click)=\"toggleSettings()\"\n    >\u2699</button>\n\n    <button\n      class=\"toolbar__btn\"\n      type=\"button\"\n      [attr.aria-pressed]=\"expanded()\"\n      [title]=\"expanded() ? 'Collapse inspector' : 'Expand inspector'\"\n      (click)=\"toggleExpanded()\"\n    >{{ expanded() ? '\u25B2' : '\u25BC' }}</button>\n  </div>\n\n  <div class=\"toolbar__stats\">\n    <span class=\"toolbar__stat\" title=\"Total change detection checks\">\n      <span class=\"toolbar__stat-label\">checks</span>\n      <strong class=\"toolbar__stat-value\">{{ tracker.totalRenders() }}</strong>\n    </span>\n    <span\n      class=\"toolbar__stat\"\n      [class.toolbar__stat--warn]=\"tracker.totalUnnecessary() > 0\"\n      title=\"Unnecessary renders: component was checked but DOM did not change\"\n    >\n      <span class=\"toolbar__stat-label\">wasted</span>\n      <strong class=\"toolbar__stat-value\">{{ tracker.totalUnnecessary() }}</strong>\n    </span>\n  </div>\n\n  @if (settingsOpen()) {\n    <div class=\"toolbar__settings\" role=\"group\" aria-label=\"Scan settings\">\n\n      <label class=\"toolbar__setting\">\n        <span class=\"toolbar__setting-label\">Enable scanning</span>\n        <button\n          class=\"toolbar__toggle\"\n          type=\"button\"\n          role=\"switch\"\n          [attr.aria-checked]=\"config.enabled()\"\n          (click)=\"toggleEnabled()\"\n          [class.toolbar__toggle--on]=\"config.enabled()\"\n        >{{ config.enabled() ? 'ON' : 'OFF' }}</button>\n      </label>\n\n      <label class=\"toolbar__setting\">\n        <span class=\"toolbar__setting-label\">Flash overlay</span>\n        <button\n          class=\"toolbar__toggle\"\n          type=\"button\"\n          role=\"switch\"\n          [attr.aria-checked]=\"config.showOverlay()\"\n          (click)=\"toggleOverlay()\"\n          [class.toolbar__toggle--on]=\"config.showOverlay()\"\n        >{{ config.showOverlay() ? 'ON' : 'OFF' }}</button>\n      </label>\n\n      <label class=\"toolbar__setting\">\n        <span class=\"toolbar__setting-label\">Render badges</span>\n        <button\n          class=\"toolbar__toggle\"\n          type=\"button\"\n          role=\"switch\"\n          [attr.aria-checked]=\"config.showBadges()\"\n          (click)=\"toggleBadges()\"\n          [class.toolbar__toggle--on]=\"config.showBadges()\"\n        >{{ config.showBadges() ? 'ON' : 'OFF' }}</button>\n      </label>\n\n      <label class=\"toolbar__setting toolbar__setting--column\">\n        <span class=\"toolbar__setting-label\">\n          Flash duration\n          <span class=\"toolbar__setting-value\">{{ config.flashDurationMs() }}ms</span>\n        </span>\n        <input\n          class=\"toolbar__slider\"\n          type=\"range\"\n          min=\"100\"\n          max=\"2000\"\n          step=\"100\"\n          [value]=\"config.flashDurationMs()\"\n          (input)=\"onFlashDurationChange($event)\"\n          aria-label=\"Flash duration in milliseconds\"\n        />\n      </label>\n\n      <button\n        class=\"toolbar__reset\"\n        type=\"button\"\n        title=\"Clear all render stats\"\n        (click)=\"resetStats()\"\n      >\u21BA Reset stats</button>\n\n    </div>\n  }\n\n  @if (expanded()) {\n    <div\n      class=\"toolbar__inspector\"\n      role=\"list\"\n      aria-label=\"Component render counts\"\n    >\n      @for (comp of tracker.trackedComponents(); track comp.hostElement) {\n        <div\n          class=\"toolbar__component\"\n          role=\"listitem\"\n          [class.toolbar__component--warn]=\"comp.lastRenderKind === 'unnecessary'\"\n          [title]=\"comp.componentName + ' \u2014 ' + comp.totalRenders + ' checks, ' + comp.unnecessaryRenders + ' wasted'\"\n        >\n          <span class=\"toolbar__component-name\">{{ comp.componentName }}</span>\n          <span class=\"toolbar__component-count\">{{ comp.totalRenders }}</span>\n          @if (comp.unnecessaryRenders > 0) {\n            <span class=\"toolbar__component-wasted\" aria-label=\"wasted renders\">\n              {{ comp.unnecessaryRenders }}W\n            </span>\n          }\n        </div>\n      }\n    </div>\n  }\n</div>\n", styles: [":host{position:fixed;bottom:0;right:0;z-index:2147483647;font-family:ui-monospace,Cascadia Code,Source Code Pro,Menlo,Consolas,monospace;font-size:11px;line-height:1.4}.toolbar{background:#0f1117;color:#c9d1d9;border:1px solid #30363d;border-bottom:none;border-right:none;border-radius:6px 0 0;min-width:200px;max-width:300px;box-shadow:-2px -2px 12px #00000080}.toolbar__header{display:flex;align-items:center;gap:4px;padding:5px 8px;border-bottom:1px solid #21262d}.toolbar__logo{color:#58a6ff;font-size:12px}.toolbar__title{font-weight:600;color:#58a6ff;flex:1;letter-spacing:.02em}.toolbar__btn{background:transparent;border:1px solid #30363d;color:#8b949e;border-radius:4px;cursor:pointer;padding:2px 6px;font-size:10px;transition:color .15s,border-color .15s}.toolbar__btn:hover{color:#c9d1d9;border-color:#58a6ff}.toolbar__btn:focus-visible{outline:2px solid #58a6ff;outline-offset:2px}.toolbar__stats{display:flex;gap:12px;padding:5px 8px}.toolbar__stat{display:flex;flex-direction:column;gap:1px}.toolbar__stat-label{color:#8b949e;font-size:9px;text-transform:uppercase;letter-spacing:.05em}.toolbar__stat-value{color:#c9d1d9}.toolbar__stat--warn .toolbar__stat-value{color:#f85149}.toolbar__inspector{max-height:220px;overflow-y:auto;border-top:1px solid #21262d}.toolbar__inspector::-webkit-scrollbar{width:4px}.toolbar__inspector::-webkit-scrollbar-track{background:transparent}.toolbar__inspector::-webkit-scrollbar-thumb{background:#30363d;border-radius:2px}.toolbar__component{display:flex;align-items:center;gap:6px;padding:3px 8px;border-bottom:1px solid #161b22;transition:background .1s}.toolbar__component:hover{background:#161b22}.toolbar__component--warn{background:#f851490f}.toolbar__component-name{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#8b949e}.toolbar__component-count{font-weight:600;color:#ffa657;min-width:20px;text-align:right}.toolbar__component-wasted{color:#f85149;font-size:9px;font-weight:600}.toolbar__settings{display:flex;flex-direction:column;gap:6px;padding:8px;border-top:1px solid #21262d}.toolbar__setting{display:flex;align-items:center;justify-content:space-between;gap:8px;cursor:default}.toolbar__setting--column{flex-direction:column;align-items:stretch;gap:4px}.toolbar__setting-label{color:#8b949e;font-size:10px;display:flex;align-items:center;justify-content:space-between;gap:4px}.toolbar__setting-value{color:#58a6ff;font-weight:600}.toolbar__toggle{background:#21262d;border:1px solid #30363d;color:#8b949e;border-radius:3px;cursor:pointer;padding:1px 6px;font-size:9px;font-weight:600;font-family:inherit;letter-spacing:.05em;min-width:32px;transition:background .15s,color .15s,border-color .15s}.toolbar__toggle--on{background:#58a6ff26;border-color:#58a6ff;color:#58a6ff}.toolbar__toggle:focus-visible{outline:2px solid #58a6ff;outline-offset:2px}.toolbar__slider{width:100%;accent-color:#58a6ff;cursor:pointer;height:4px}.toolbar__reset{background:transparent;border:1px solid #30363d;color:#8b949e;border-radius:4px;cursor:pointer;padding:3px 8px;font-size:10px;font-family:inherit;margin-top:2px;transition:color .15s,border-color .15s;align-self:flex-start}.toolbar__reset:hover{color:#f85149;border-color:#f85149}.toolbar__reset:focus-visible{outline:2px solid #58a6ff;outline-offset:2px}\n"] }]
        }] });

/**
 * Provides angular-scan for development-time render tracking.
 *
 * Add to your application providers:
 * ```ts
 * bootstrapApplication(AppComponent, {
 *   providers: [provideAngularScan()]
 * });
 * ```
 *
 * This is a complete no-op in production builds (`isDevMode() === false`),
 * so it can safely be left in your app config.
 */
function provideAngularScan(options = {}) {
    // Return empty providers in production — entire library is tree-shaken
    if (!isDevMode()) {
        return makeEnvironmentProviders([]);
    }
    const resolvedOptions = {
        enabled: true,
        flashDurationMs: 500,
        showBadges: true,
        showToolbar: true,
        ...options,
    };
    return makeEnvironmentProviders([
        {
            provide: ANGULAR_SCAN_OPTIONS,
            useValue: resolvedOptions,
        },
        provideEnvironmentInitializer(() => {
            const overlay = inject(OverlayService);
            const scanner = inject(ScannerService);
            const opts = inject(ANGULAR_SCAN_OPTIONS);
            overlay.initialize();
            scanner.initialize();
            if (opts.showToolbar !== false) {
                const injector = inject(EnvironmentInjector);
                const doc = inject(DOCUMENT$1);
                const appRef = inject(ApplicationRef);
                // Create the toolbar outside Angular's component tree so it doesn't
                // appear in CD tracking. Attach it to ApplicationRef so signals work.
                const toolbarRef = createComponent(ToolbarComponent, {
                    environmentInjector: injector,
                });
                doc.body.appendChild(toolbarRef.location.nativeElement);
                appRef.attachView(toolbarRef.hostView);
                // Tell the scanner to ignore the toolbar's own renders
                scanner.setToolbarInstance(toolbarRef.instance);
            }
        }),
    ]);
}

// Profiler event constants
const TemplateUpdateStart = 2;
const ChangeDetectionStart = 12;
const ChangeDetectionEnd = 13;
const ChangeDetectionSyncStart = 14;
const ChangeDetectionSyncEnd = 15;
/**
 * Imperative API for angular-scan.
 *
 * Use this when you cannot modify application providers (e.g. micro-frontends,
 * third-party apps). Call before `bootstrapApplication()`.
 *
 * ```ts
 * // main.ts
 * import { scan } from 'angular-scan';
 * scan();
 * bootstrapApplication(AppComponent, appConfig);
 * ```
 *
 * @returns A teardown function that stops scanning and removes the overlay.
 */
function scan(options = {}) {
    if (!isDevMode())
        return noop;
    if (typeof window === 'undefined')
        return noop;
    if (options.enabled === false)
        return noop;
    const ng = getNgDebugApi();
    if (!ng) {
        console.warn('[angular-scan] Angular debug APIs (window.ng) not available. ' +
            'Ensure you are running in development mode.');
        return noop;
    }
    const durationMs = options.flashDurationMs ?? 500;
    const showBadges = options.showBadges !== false;
    const overlay = new CanvasOverlay();
    overlay.attach(document);
    // State for the current tick
    let inTick = false;
    let inSyncPhase = false;
    const mutatedNodes = new Set();
    const tickInstances = new Set();
    const renderCounts = new WeakMap();
    const unnecessaryCounts = new WeakMap();
    const badges = new Map();
    const observer = new MutationObserver(records => {
        for (const r of records) {
            mutatedNodes.add(r.target);
            r.addedNodes.forEach(n => mutatedNodes.add(n));
            r.removedNodes.forEach(n => mutatedNodes.add(n));
        }
    });
    const removeProfiler = ng.ɵsetProfiler((event, instance) => {
        switch (event) {
            case ChangeDetectionStart:
                if (inTick)
                    break;
                inTick = true;
                mutatedNodes.clear();
                tickInstances.clear();
                observer.observe(document.body, {
                    subtree: true, childList: true, attributes: true, characterData: true,
                });
                break;
            case ChangeDetectionSyncStart:
                inSyncPhase = true;
                break;
            case ChangeDetectionSyncEnd:
                inSyncPhase = false;
                break;
            case TemplateUpdateStart:
                if (inSyncPhase && instance)
                    tickInstances.add(instance);
                break;
            case ChangeDetectionEnd: {
                if (!inTick)
                    break;
                inTick = false;
                inSyncPhase = false;
                const pending = observer.takeRecords();
                for (const r of pending) {
                    mutatedNodes.add(r.target);
                    r.addedNodes.forEach(n => mutatedNodes.add(n));
                    r.removedNodes.forEach(n => mutatedNodes.add(n));
                }
                observer.disconnect();
                // Build mutated host set
                const mutatedHosts = new Set();
                for (const node of mutatedNodes) {
                    let current = node;
                    while (current && current !== document.body) {
                        if (current instanceof Element) {
                            try {
                                if (ng.getComponent(current)) {
                                    mutatedHosts.add(current);
                                    break;
                                }
                            }
                            catch { /* ignore */ }
                        }
                        current = current.parentNode;
                    }
                }
                // Collect updates without writing to any signals
                const updates = [];
                for (const inst of tickInstances) {
                    try {
                        const hostEl = ng.getHostElement(inst);
                        if (!hostEl)
                            continue;
                        const kind = mutatedHosts.has(hostEl) ? 'render' : 'unnecessary';
                        updates.push({ instance: inst, hostEl, kind });
                    }
                    catch { /* component destroyed */ }
                }
                queueMicrotask(() => {
                    for (const { instance, hostEl, kind } of updates) {
                        const count = (renderCounts.get(instance) ?? 0) + 1;
                        renderCounts.set(instance, count);
                        if (kind === 'unnecessary') {
                            unnecessaryCounts.set(instance, (unnecessaryCounts.get(instance) ?? 0) + 1);
                        }
                        overlay.flash(hostEl, kind, durationMs);
                        if (showBadges)
                            updateBadge(hostEl, count, kind);
                    }
                });
                break;
            }
        }
    });
    function updateBadge(hostEl, count, kind) {
        let badge = badges.get(hostEl);
        if (!badge) {
            badge = document.createElement('div');
            badge.setAttribute('aria-hidden', 'true');
            badge.setAttribute('role', 'presentation');
            badge.style.cssText = [
                'position:absolute', 'top:2px', 'right:2px',
                'z-index:2147483645', 'pointer-events:none',
                'font:bold 9px/13px monospace', 'padding:1px 4px',
                'border-radius:3px', 'min-width:16px', 'text-align:center', 'color:#fff',
            ].join(';');
            const htmlEl = hostEl;
            if (getComputedStyle(htmlEl).position === 'static')
                htmlEl.style.position = 'relative';
            hostEl.appendChild(badge);
            badges.set(hostEl, badge);
        }
        badge.style.background = kind === 'unnecessary' ? '#f44336' : '#ff9800';
        badge.textContent = String(count);
    }
    return () => {
        removeProfiler();
        observer.disconnect();
        overlay.detach();
        for (const b of badges.values())
            b.remove();
        badges.clear();
    };
}
function noop() { }

/*
 * Public API Surface of angular-scan
 */

/**
 * Generated bundle index. Do not edit.
 */

export { provideAngularScan, scan };
//# sourceMappingURL=angular-scan.mjs.map
