UNPKG

wacom

Version:

Module which has common services, pipes, directives and interfaces which can be used on all projects.

4,122 lines 156 kB
import * as i0 from '@angular/core';
import { InjectionToken, Inject, Optional, Injectable, signal, inject, ChangeDetectorRef, output, ElementRef, DestroyRef, Directive, input, effect, Pipe, isSignal, ApplicationRef, EnvironmentInjector, createComponent, makeEnvironmentProviders, NgModule } from '@angular/core';
import * as i1 from '@angular/router';
import * as i2 from '@angular/platform-browser';
import { DomSanitizer } from '@angular/platform-browser';
import { take, firstValueFrom, Subject, skip, takeUntil, share, filter, map, Observable, merge, combineLatest, timeout, ReplaySubject, EMPTY } from 'rxjs';
import { toObservable } from '@angular/core/rxjs-interop';
import * as i1$1 from '@angular/common/http';
import { HttpHeaders, HttpErrorResponse, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
import { first, catchError } from 'rxjs/operators';
import * as i1$2 from '@angular/common';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';

const CONFIG_TOKEN = new InjectionToken('config');
const DEFAULT_CONFIG = {
    store: {
        prefix: 'waStore',
    },
    meta: {
        useTitleSuffix: false,
        warnMissingGuard: false,
        defaults: { links: {} },
    },
    socket: false,
    http: {
        url: '',
        headers: {},
    },
};

const DEFAULT_HTTP_CONFIG = {
    headers: {},
    url: '',
};

const DEFAULT_NETWORK_CONFIG = {
    endpoints: [
        'https://api.webart.work/status',
        // Opaque but useful reachability fallbacks:
        'https://www.google.com/generate_204',
        'https://www.gstatic.com/generate_204',
        'https://www.cloudflare.com/cdn-cgi/trace',
    ],
    intervalMs: 30_000,
    timeoutMs: 2_500,
    goodLatencyMs: 300,
    maxConsecutiveFails: 3,
};
const NETWORK_CONFIG = new InjectionToken('NETWORK_CONFIG', {
    factory: () => DEFAULT_NETWORK_CONFIG,
});

const isDefined = (val) => typeof val !== 'undefined';
class MetaService {
    constructor(config, router, meta, titleService) {
        this.config = config;
        this.router = router;
        this.meta = meta;
        this.titleService = titleService;
        this.config = this.config || DEFAULT_CONFIG;
        this._meta = this.config.meta || {};
        this._warnMissingGuard();
    }
    /**
     * Sets the default meta tags.
     *
     * @param defaults - The default meta tags.
     */
    setDefaults(defaults) {
        this._meta.defaults = {
            ...this._meta.defaults,
            ...defaults,
        };
    }
    /**
     * Sets the title and optional title suffix.
     *
     * @param title - The title to set.
     * @param titleSuffix - The title suffix to append.
     * @returns The MetaService instance.
     */
    setTitle(title, titleSuffix) {
        let titleContent = isDefined(title)
            ? title || ''
            : this._meta.defaults?.['title'] || '';
        if (this._meta.useTitleSuffix) {
            titleContent += isDefined(titleSuffix)
                ? titleSuffix
                : this._meta.defaults?.['titleSuffix'] || '';
        }
        this._updateMetaTag('title', titleContent);
        this._updateMetaTag('og:title', titleContent);
        this._updateMetaTag('twitter:title', titleContent);
        this.titleService.setTitle(titleContent);
        return this;
    }
    /**
     * Sets link tags.
     *
     * @param links - The links to set.
     * @returns The MetaService instance.
     */
    setLink(links) {
        Object.keys(links).forEach((rel) => {
            let link = document.createElement('link');
            link.setAttribute('rel', rel);
            link.setAttribute('href', links[rel]);
            document.head.appendChild(link);
        });
        return this;
    }
    /**
     * Sets a meta tag.
     *
     * @param tag - The meta tag name.
     * @param value - The meta tag value.
     * @param prop - The meta tag property.
     */
    setTag(tag, value, prop) {
        if (tag === 'title' || tag === 'titleSuffix') {
            throw new Error(`Attempt to set ${tag} through 'setTag': 'title' and 'titleSuffix' are reserved. Use 'MetaService.setTitle' instead.`);
        }
        const content = (isDefined(value)
            ? value || ''
            : this._meta.defaults?.[tag] || '') + '';
        this._updateMetaTag(tag, content, prop);
        if (tag === 'description') {
            this._updateMetaTag('og:description', content, prop);
            this._updateMetaTag('twitter:description', content, prop);
        }
    }
    /**
     * Updates a meta tag.
     *
     * @param tag - The meta tag name.
     * @param value - The meta tag value.
     * @param prop - The meta tag property.
     */
    _updateMetaTag(tag, value, prop) {
        prop =
            prop ||
                (tag.startsWith('og:') || tag.startsWith('twitter:')
                    ? 'property'
                    : 'name');
        this.meta.updateTag({ [prop]: tag, content: value });
    }
    /**
     * Removes a meta tag.
     *
     * @param tag - The meta tag name.
     * @param prop - The meta tag property.
     */
    removeTag(tag, prop) {
        prop =
            prop ||
                (tag.startsWith('og:') || tag.startsWith('twitter:')
                    ? 'property'
                    : 'name');
        this.meta.removeTag(`${prop}="${tag}"`);
    }
    /**
     * Warns about missing meta guards in routes.
     */
    _warnMissingGuard() {
        if (isDefined(this._meta.warnMissingGuard) &&
            !this._meta.warnMissingGuard) {
            return;
        }
        const hasDefaultMeta = !!Object.keys(this._meta.defaults ?? {}).length;
        const hasMetaGuardInArr = (it) => it && it.IDENTIFIER === 'MetaGuard';
        let hasShownWarnings = false;
        const checkRoute = (route) => {
            const hasRouteMeta = route.data && route.data['meta'];
            const showWarning = !isDefined(route.redirectTo) &&
                (hasDefaultMeta || hasRouteMeta) &&
                !(route.canActivate || []).some(hasMetaGuardInArr);
            if (showWarning) {
                console.warn(`Route with path "${route.path}" has ${hasRouteMeta ? '' : 'default '}meta tags, but does not use MetaGuard. Please add MetaGuard to the canActivate array in your route configuration`);
                hasShownWarnings = true;
            }
            (route.children || []).forEach(checkRoute);
        };
        this.router.config.forEach(checkRoute);
        if (hasShownWarnings) {
            console.warn(`To disable these warnings, set metaConfig.warnMissingGuard: false in your MetaConfig passed to MetaModule.forRoot()`);
        }
    }
    static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: MetaService, deps: [{ token: CONFIG_TOKEN, optional: true }, { token: i1.Router }, { token: i2.Meta }, { token: i2.Title }], target: i0.ɵɵFactoryTarget.Injectable }); }
    static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: MetaService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: MetaService, decorators: [{
            type: Injectable,
            args: [{
                    providedIn: 'root',
                }]
        }], ctorParameters: () => [{ type: undefined, decorators: [{
                    type: Inject,
                    args: [CONFIG_TOKEN]
                }, {
                    type: Optional
                }] }, { type: i1.Router }, { type: i2.Meta }, { type: i2.Title }] });

class MetaGuard {
    static { this.IDENTIFIER = 'MetaGuard'; }
    constructor(metaService, config) {
        this.metaService = metaService;
        this.config = config;
        if (!this.config)
            this.config = DEFAULT_CONFIG;
        this._meta = this.config.meta || {};
        this._meta.defaults = this._meta.defaults || {};
    }
    canActivate(route, state) {
        this._processRouteMetaTags(route.data && route.data['meta']);
        return true;
    }
    _processRouteMetaTags(meta = {}) {
        if (meta.disableUpdate) {
            return;
        }
        if (meta.title) {
            this.metaService.setTitle(meta.title, meta.titleSuffix);
        }
        if (meta.links && Object.keys(meta.links).length) {
            this.metaService.setLink(meta.links);
        }
        if (this._meta.defaults?.links &&
            Object.keys(this._meta.defaults.links).length) {
            this.metaService.setLink(this._meta.defaults.links);
        }
        Object.keys(meta).forEach((prop) => {
            if (prop === 'title' ||
                prop === 'titleSuffix' ||
                prop === 'links') {
                return;
            }
            Object.keys(meta[prop]).forEach((key) => {
                this.metaService.setTag(key, meta[prop][key], prop);
            });
        });
        Object.keys(this._meta.defaults).forEach((key) => {
            if (key in meta ||
                key === 'title' ||
                key === 'titleSuffix' ||
                key === 'links') {
                return;
            }
            this.metaService.setTag(key, this._meta.defaults[key]);
        });
    }
    static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: MetaGuard, deps: [{ token: MetaService }, { token: CONFIG_TOKEN, optional: true }], target: i0.ɵɵFactoryTarget.Injectable }); }
    static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: MetaGuard, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: MetaGuard, decorators: [{
            type: Injectable,
            args: [{ providedIn: 'root' }]
        }], ctorParameters: () => [{ type: MetaService }, { type: undefined, decorators: [{
                    type: Inject,
                    args: [CONFIG_TOKEN]
                }, {
                    type: Optional
                }] }] });

// Core utilities and helpers for the Wacom app
// Add capitalize method to String prototype if it doesn't already exist
if (!String.prototype.capitalize) {
    String.prototype.capitalize = function () {
        if (this.length > 0) {
            return this.charAt(0).toUpperCase() + this.slice(1).toLowerCase();
        }
        return '';
    };
}
class CoreService {
    constructor() {
        this.deviceID = localStorage.getItem('deviceID') ||
            (typeof crypto?.randomUUID === 'function'
                ? crypto.randomUUID()
                : this.UUID());
        // After While
        this._afterWhile = {};
        // Device management
        this.device = '';
        // Version management
        this.version = '1.0.0';
        this.appVersion = '';
        this.dateVersion = '';
        // Locking management
        this._locked = {};
        this._unlockResolvers = {};
        localStorage.setItem('deviceID', this.deviceID);
        this.detectDevice();
    }
    /**
     * Generates a UUID (Universally Unique Identifier) version 4.
     *
     * This implementation uses `Math.random()` to generate random values,
     * making it suitable for general-purpose identifiers, but **not** for
     * cryptographic or security-sensitive use cases.
     *
     * The format follows the UUID v4 standard: `xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx`
     * where:
     * - `x` is a random hexadecimal digit (0–f)
     * - `4` indicates UUID version 4
     * - `y` is one of 8, 9, A, or B
     *
     * Example: `f47ac10b-58cc-4372-a567-0e02b2c3d479`
     *
     * @returns A string containing a UUID v4.
     */
    UUID() {
        return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
            const r = (Math.random() * 16) | 0;
            const v = c === 'x' ? r : (r & 0x3) | 0x8;
            return v.toString(16);
        });
    }
    /**
     * Converts an object to an array. Optionally holds keys instead of values.
     *
     * @param {any} obj - The object to be converted.
     * @param {boolean} [holder=false] - If true, the keys will be held in the array; otherwise, the values will be held.
     * @returns {any[]} The resulting array.
     */
    ota(obj, holder = false) {
        if (Array.isArray(obj))
            return obj;
        if (typeof obj !== 'object' || obj === null)
            return [];
        const arr = [];
        for (const each in obj) {
            if (obj.hasOwnProperty(each) &&
                (obj[each] ||
                    typeof obj[each] === 'number' ||
                    typeof obj[each] === 'boolean')) {
                if (holder) {
                    arr.push(each);
                }
                else {
                    arr.push(obj[each]);
                }
            }
        }
        return arr;
    }
    /**
     * Removes elements from `fromArray` that are present in `removeArray` based on a comparison field.
     *
     * @param {any[]} removeArray - The array of elements to remove.
     * @param {any[]} fromArray - The array from which to remove elements.
     * @param {string} [compareField='_id'] - The field to use for comparison.
     * @returns {any[]} The modified `fromArray` with elements removed.
     */
    splice(removeArray, fromArray, compareField = '_id') {
        if (!Array.isArray(removeArray) || !Array.isArray(fromArray)) {
            return fromArray;
        }
        const removeSet = new Set(removeArray.map((item) => item[compareField]));
        return fromArray.filter((item) => !removeSet.has(item[compareField]));
    }
    /**
     * Unites multiple _id values into a single unique _id.
     * The resulting _id is unique regardless of the order of the input _id values.
     *
     * @param {...string[]} args - The _id values to be united.
     * @returns {string} The unique combined _id.
     */
    ids2id(...args) {
        args.sort((a, b) => {
            if (Number(a.toString().substring(0, 8)) >
                Number(b.toString().substring(0, 8))) {
                return 1;
            }
            return -1;
        });
        return args.join();
    }
    /**
     * Delays the execution of a callback function for a specified amount of time.
     * If called again within that time, the timer resets.
     *
     * @param {string | object | (() => void)} doc - A unique identifier for the timer, an object to host the timer, or the callback function.
     * @param {() => void} [cb] - The callback function to execute after the delay.
     * @param {number} [time=1000] - The delay time in milliseconds.
     */
    afterWhile(doc, cb, time = 1000) {
        if (typeof doc === 'function') {
            cb = doc;
            doc = 'common';
        }
        if (typeof cb === 'function' && typeof time === 'number') {
            if (typeof doc === 'string') {
                clearTimeout(this._afterWhile[doc]);
                this._afterWhile[doc] = window.setTimeout(cb, time);
            }
            else if (typeof doc === 'object') {
                clearTimeout(doc.__afterWhile);
                doc.__afterWhile =
                    window.setTimeout(cb, time);
            }
            else {
                console.warn('badly configured after while');
            }
        }
    }
    /**
     * Recursively copies properties from one object to another.
     * Handles nested objects, arrays, and Date instances appropriately.
     *
     * @param from - The source object from which properties are copied.
     * @param to - The target object to which properties are copied.
     */
    copy(from, to) {
        for (const each in from) {
            if (typeof from[each] !== 'object' ||
                from[each] instanceof Date ||
                Array.isArray(from[each]) ||
                from[each] === null) {
                to[each] = from[each];
            }
            else {
                if (typeof to[each] !== 'object' ||
                    to[each] instanceof Date ||
                    Array.isArray(to[each]) ||
                    to[each] === null) {
                    to[each] = {};
                }
                this.copy(from[each], to[each]);
            }
        }
    }
    /**
     * Detects the device type based on the user agent.
     */
    detectDevice() {
        const userAgent = navigator.userAgent || navigator.vendor || window.opera;
        if (/windows phone/i.test(userAgent)) {
            this.device = 'Windows Phone';
        }
        else if (/android/i.test(userAgent)) {
            this.device = 'Android';
        }
        else if (/iPad|iPhone|iPod/.test(userAgent) &&
            !window.MSStream) {
            this.device = 'iOS';
        }
        else {
            this.device = 'Web';
        }
    }
    /**
     * Checks if the device is a mobile device.
     * @returns {boolean} - Returns true if the device is a mobile device.
     */
    isMobile() {
        return (this.device === 'Windows Phone' ||
            this.device === 'Android' ||
            this.device === 'iOS');
    }
    /**
     * Checks if the device is a tablet.
     * @returns {boolean} - Returns true if the device is a tablet.
     */
    isTablet() {
        return this.device === 'iOS' && /iPad/.test(navigator.userAgent);
    }
    /**
     * Checks if the device is a web browser.
     * @returns {boolean} - Returns true if the device is a web browser.
     */
    isWeb() {
        return this.device === 'Web';
    }
    /**
     * Checks if the device is an Android device.
     * @returns {boolean} - Returns true if the device is an Android device.
     */
    isAndroid() {
        return this.device === 'Android';
    }
    /**
     * Checks if the device is an iOS device.
     * @returns {boolean} - Returns true if the device is an iOS device.
     */
    isIos() {
        return this.device === 'iOS';
    }
    /**
     * Sets the combined version string based on appVersion and dateVersion.
     */
    setVersion() {
        this.version = this.appVersion || '';
        this.version += this.version && this.dateVersion ? ' ' : '';
        this.version += this.dateVersion || '';
    }
    /**
     * Sets the app version and updates the combined version string.
     *
     * @param {string} appVersion - The application version to set.
     */
    setAppVersion(appVersion) {
        this.appVersion = appVersion;
        this.setVersion();
    }
    /**
     * Sets the date version and updates the combined version string.
     *
     * @param {string} dateVersion - The date version to set.
     */
    setDateVersion(dateVersion) {
        this.dateVersion = dateVersion;
        this.setVersion();
    }
    /**
     * Locks a resource to prevent concurrent access.
     * @param which - The resource to lock, identified by a string.
     */
    lock(which) {
        this._locked[which] = true;
        if (!this._unlockResolvers[which]) {
            this._unlockResolvers[which] = [];
        }
    }
    /**
     * Unlocks a resource, allowing access.
     * @param which - The resource to unlock, identified by a string.
     */
    unlock(which) {
        this._locked[which] = false;
        if (this._unlockResolvers[which]) {
            this._unlockResolvers[which].forEach((resolve) => resolve());
            this._unlockResolvers[which] = [];
        }
    }
    /**
     * Returns a Promise that resolves when the specified resource is unlocked.
     * @param which - The resource to watch for unlocking, identified by a string.
     * @returns A Promise that resolves when the resource is unlocked.
     */
    onUnlock(which) {
        if (!this._locked[which]) {
            return Promise.resolve();
        }
        return new Promise((resolve) => {
            if (!this._unlockResolvers[which]) {
                this._unlockResolvers[which] = [];
            }
            this._unlockResolvers[which].push(resolve);
        });
    }
    /**
     * Checks if a resource is locked.
     * @param which - The resource to check, identified by a string.
     * @returns True if the resource is locked, false otherwise.
     */
    locked(which) {
        return !!this._locked[which];
    }
    // Angular Signals //
    /**
     * Converts a plain object into a signal-wrapped object.
     * Optionally wraps specific fields of the object as individual signals,
     * and merges them into the returned signal for fine-grained reactivity.
     *
     * @template Document - The type of the object being wrapped.
     * @param {Document} document - The plain object to wrap into a signal.
     * @param {Record<string, (doc: Document) => unknown>} [signalFields={}] -
     *        Optional map where each key is a field name and the value is a function
     *        to extract the initial value for that field. These fields will be wrapped
     *        as separate signals and embedded in the returned object.
     *
     * @returns {WritableSignal<Document>} A signal-wrapped object, possibly containing
     *          nested field signals for more granular control.
     *
     * @example
     * const user = { _id: '1', name: 'Alice', score: 42 };
     * const sig = toSignal(user, { score: (u) => u.score });
     * console.log(sig().name); // 'Alice'
     * console.log(sig().score()); // 42 — field is now a signal
     */
    toSignal(document, signalFields = {}) {
        if (Object.keys(signalFields).length) {
            const fields = {};
            for (const key in signalFields) {
                fields[key] = signal(signalFields[key](document));
            }
            return signal({ ...document, ...fields });
        }
        else {
            return signal(document);
        }
    }
    /**
     * Converts an array of objects into an array of Angular signals.
     * Optionally wraps specific fields of each object as individual signals.
     *
     * @template Document - The type of each object in the array.
     * @param {Document[]} arr - Array of plain objects to convert into signals.
     * @param {Record<string, (doc: Document) => unknown>} [signalFields={}] -
     *        Optional map where keys are field names and values are functions that extract the initial value
     *        from the object. These fields will be turned into separate signals.
     *
     * @returns {WritableSignal<Document>[]} An array where each item is a signal-wrapped object,
     *          optionally with individual fields also wrapped in signals.
     *
     * @example
     * toSignalsArray(users, {
     *   name: (u) => u.name,
     *   score: (u) => u.score,
     * });
     */
    toSignalsArray(arr, signalFields = {}) {
        return arr.map((obj) => this.toSignal(obj, signalFields));
    }
    /**
     * Adds a new object to the signals array.
     * Optionally wraps specific fields of the object as individual signals before wrapping the whole object.
     *
     * @template Document - The type of the object being added.
     * @param {WritableSignal<Document>[]} signals - The signals array to append to.
     * @param {Document} item - The object to wrap and push as a signal.
     * @param {Record<string, (doc: Document) => unknown>} [signalFields={}] -
     *        Optional map of fields to be wrapped as signals within the object.
     *
     * @returns {void}
     */
    pushSignal(signals, item, signalFields = {}) {
        signals.push(this.toSignal(item, signalFields));
    }
    /**
     * Removes the first signal from the array whose object's field matches the provided value.
     * @template Document
     * @param {WritableSignal<Document>[]} signals - The signals array to modify.
     * @param {unknown} value - The value to match.
     * @param {string} [field='_id'] - The object field to match against.
     * @returns {void}
     */
    removeSignalByField(signals, value, field = '_id') {
        const idx = signals.findIndex((sig) => sig()[field] === value);
        if (idx > -1)
            signals.splice(idx, 1);
    }
    /**
     * Returns a generic trackBy function for *ngFor, tracking by the specified object field.
     * @template Document
     * @param {string} field - The object field to use for tracking (e.g., '_id').
     * @returns {(index: number, sig: Signal<Document>) => unknown} TrackBy function for Angular.
     */
    trackBySignalField(field) {
        return (_, sig) => sig()[field];
    }
    /**
     * Finds the first signal in the array whose object's field matches the provided value.
     * @template Document
     * @param {Signal<Document>[]} signals - Array of signals to search.
     * @param {unknown} value - The value to match.
     * @param {string} [field='_id'] - The object field to match against.
     * @returns {Signal<Document> | undefined} The found signal or undefined if not found.
     */
    findSignalByField(signals, value, field = '_id') {
        return signals.find((sig) => sig()[field] === value);
    }
    /**
     * Updates the first writable signal in the array whose object's field matches the provided value.
     * @template Document
     * @param {WritableSignal<Document>[]} signals - Array of writable signals to search.
     * @param {unknown} value - The value to match.
     * @param {(val: Document) => Document} updater - Function to produce the updated object.
     * @param {string} field - The object field to match against.
     * @returns {void}
     */
    updateSignalByField(signals, value, updater, field) {
        const sig = this.findSignalByField(signals, value, field);
        if (sig)
            sig.update(updater);
    }
    static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: CoreService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
    static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: CoreService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: CoreService, decorators: [{
            type: Injectable,
            args: [{
                    providedIn: 'root',
                }]
        }], ctorParameters: () => [] });

/**
 * Abstract reusable base class for CRUD list views.
 * It encapsulates pagination, modals, and document handling logic.
 *
 * @template Service - A service implementing CrudServiceInterface for a specific document type
 * @template Document - The data model extending CrudDocument
 */
class CrudComponent {
    /**
     * Constructor
     *
     * @param formConfig - Object describing form title and its component structure
     * @param formService - Any service that conforms to FormServiceInterface (usually casted)
     * @param crudService - CRUD service implementing get/create/update/delete
     */
    constructor(formConfig, formService, crudService, module = '') {
        /** The array of documents currently loaded and shown */
        this.documents = signal([], ...(ngDevMode ? [{ debugName: "documents" }] : []));
        /** Current pagination page */
        this.page = 1;
        /** CoreService handles timing and copying helpers */
        this.__core = inject(CoreService);
        /** ChangeDetectorRef handles on push strategy */
        this.__cdr = inject(ChangeDetectorRef);
        this.localDocumentsFilter = () => true;
        /** Fields considered when performing bulk updates. */
        this.updatableFields = ['_id', 'name', 'description', 'data'];
        /** Data source mode used for document retrieval. */
        this.configType = 'server';
        /** Number of documents fetched per page when paginating. */
        this.perPage = 20;
        /** Name of the collection or module used for contextual actions. */
        this._module = '';
        const form = formConfig;
        this.__form = formService;
        this.form = form;
        this.crudService = crudService;
        this._module = module;
    }
    /**
     * Loads documents for a given page.
     */
    setDocuments(page = this.page, query = '') {
        return new Promise((resolve) => {
            if (this.configType === 'server') {
                this.page = page;
                this.__core.afterWhile(this, () => {
                    this.crudService
                        .get({ page, query }, this.getOptions())
                        .subscribe((docs) => {
                        this.documents.update(() => docs.map((doc) => this.crudService.getSignal(doc)));
                        resolve();
                        this.__cdr.markForCheck();
                    });
                }, 250);
            }
            else {
                this.documents.update(() => this.crudService
                    .getDocs()
                    .filter(this.localDocumentsFilter)
                    .map((doc) => this.crudService.getSignal(doc)));
                this.crudService.loaded.pipe(take(1)).subscribe(() => {
                    resolve();
                    this.__cdr.markForCheck();
                });
            }
        });
    }
    /**
     * Clears temporary metadata before document creation.
     */
    preCreate(doc) {
        delete doc.__creating;
    }
    /**
     * Funciton which controls whether the create functionality is available.
     */
    allowCreate() {
        return true;
    }
    /**
     * Funciton which controls whether the update and delete functionality is available.
     */
    allowMutate() {
        return true;
    }
    /**
     * Funciton which controls whether the unique url functionality is available.
     */
    allowUrl() {
        return true;
    }
    /** Determines whether manual sorting controls are available. */
    allowSort() {
        return false;
    }
    /**
     * Funciton which prepare get crud options.
     */
    getOptions() {
        return {};
    }
    /**
     * Handles bulk creation and updating of documents.
     * In creation mode, adds new documents.
     * In update mode, syncs changes and deletes removed entries.
     */
    bulkManagement(isCreateFlow = true) {
        return () => {
            this.__form
                .modalDocs(isCreateFlow
                ? []
                : this.documents().map((obj) => Object.fromEntries(this.updatableFields.map((key) => [
                    key,
                    obj()[key],
                ]))))
                .then(async (docs) => {
                if (isCreateFlow) {
                    for (const doc of docs) {
                        this.preCreate(doc);
                        await firstValueFrom(this.crudService.create(doc));
                    }
                }
                else {
                    for (const document of this.documents()) {
                        if (!docs.find((d) => d._id === document()._id)) {
                            await firstValueFrom(this.crudService.delete(document()));
                        }
                    }
                    for (const doc of docs) {
                        const local = this.documents().find((document) => document()._id === doc._id);
                        if (local) {
                            local.update((document) => {
                                this.__core.copy(doc, document);
                                return document;
                            });
                            await firstValueFrom(this.crudService.update(local()));
                        }
                        else {
                            this.preCreate(doc);
                            await firstValueFrom(this.crudService.create(doc));
                        }
                    }
                }
                this.setDocuments();
            });
        };
    }
    /** Opens a modal to create a new document. */
    create() {
        this.__form.modal(this.form, {
            label: 'Create',
            click: async (created, close) => {
                close();
                this.preCreate(created);
                await firstValueFrom(this.crudService.create(created));
                this.setDocuments();
            },
        });
    }
    /** Displays a modal to edit an existing document. */
    update(doc) {
        this.__form.modal(this.form, {
            label: 'Update',
            click: (updated, close) => {
                close();
                this.__core.copy(updated, doc);
                this.crudService.update(doc);
                this.__cdr.markForCheck();
            },
        }, doc);
    }
    /** Requests confirmation before deleting the provided document. */
    async delete(doc) {
        this.crudService.delete(doc).subscribe(() => {
            this.setDocuments();
        });
    }
    /** Opens a modal to edit the document's unique URL. */
    mutateUrl(doc) {
        this.__form.modalUnique(this._module, 'url', doc);
    }
    /** Moves the given document one position up and updates ordering. */
    moveUp(doc) {
        const index = this.documents().findIndex((document) => document()._id === doc._id);
        if (index) {
            this.documents.update((documents) => {
                documents.splice(index, 1);
                documents.splice(index - 1, 0, this.crudService.getSignal(doc));
                return documents;
            });
        }
        for (let i = 0; i < this.documents().length; i++) {
            if (this.documents()[i]().order !== i) {
                this.documents()[i]().order = i;
                this.crudService.update(this.documents()[i]());
            }
        }
        this.__cdr.markForCheck();
    }
    /**
     * Configuration object used by the UI for rendering table and handling actions.
     */
    getConfig() {
        const config = {
            create: this.allowCreate()
                ? () => {
                    this.create();
                }
                : null,
            update: this.allowMutate()
                ? (doc) => {
                    this.update(doc);
                }
                : null,
            delete: this.allowMutate()
                ? (doc) => {
                    this.delete(doc);
                }
                : null,
            buttons: [],
            headerButtons: [],
            allDocs: true,
        };
        if (this.allowUrl()) {
            config.buttons.push({
                icon: 'cloud_download',
                click: (doc) => {
                    this.mutateUrl(doc);
                },
            });
        }
        if (this.allowSort()) {
            config.buttons.push({
                icon: 'arrow_upward',
                click: (doc) => {
                    this.moveUp(doc);
                },
            });
        }
        if (this.allowCreate()) {
            config.headerButtons.push({
                icon: 'playlist_add',
                click: this.bulkManagement(),
                class: 'playlist',
            });
        }
        if (this.allowMutate()) {
            config.headerButtons.push({
                icon: 'edit_note',
                click: this.bulkManagement(false),
                class: 'edit',
            });
        }
        return this.configType === 'server'
            ? {
                ...config,
                paginate: this.setDocuments.bind(this),
                perPage: this.perPage,
                setPerPage: this.crudService.setPerPage?.bind(this.crudService),
                allDocs: false,
            }
            : config;
    }
}

/**
 * Stand-alone “click outside” directive (zoneless-safe).
 *
 * Usage:
 * <div (clickOutside)="close()">…</div>
 */
class ClickOutsideDirective {
    constructor() {
        this.clickOutside = output();
        this._host = inject((ElementRef));
        this._cdr = inject(ChangeDetectorRef);
        this._dref = inject(DestroyRef);
        this.handler = (e) => {
            if (!this._host.nativeElement.contains(e.target)) {
                this.clickOutside.emit(e); // notify parent
                this._cdr.markForCheck(); // trigger CD for OnPush comps
            }
        };
        document.addEventListener('pointerdown', this.handler, true);
        // cleanup
        this._dref.onDestroy(() => document.removeEventListener('pointerdown', this.handler, true));
    }
    static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: ClickOutsideDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
    static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.0", type: ClickOutsideDirective, isStandalone: true, selector: "[clickOutside]", outputs: { clickOutside: "clickOutside" }, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: ClickOutsideDirective, decorators: [{
            type: Directive,
            args: [{
                    selector: '[clickOutside]',
                }]
        }], ctorParameters: () => [], propDecorators: { clickOutside: [{ type: i0.Output, args: ["clickOutside"] }] } });

class ManualDisabledDirective {
    constructor() {
        this.el = inject(ElementRef);
        // Bind as: [manualDisabled]="isDisabled"
        this.manualDisabled = input(null, { ...(ngDevMode ? { debugName: "manualDisabled" } : {}), alias: 'manualDisabled' });
        this.syncDisabledEffect = effect(() => {
            const disabled = this.manualDisabled();
            if (disabled == null)
                return;
            const native = this.el.nativeElement;
            if (!native)
                return;
            native.disabled = !!disabled;
        }, ...(ngDevMode ? [{ debugName: "syncDisabledEffect" }] : []));
    }
    static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: ManualDisabledDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
    static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.1.0", type: ManualDisabledDirective, isStandalone: true, selector: "input[manualDisabled], textarea[manualDisabled]", inputs: { manualDisabled: { classPropertyName: "manualDisabled", publicName: "manualDisabled", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: ManualDisabledDirective, decorators: [{
            type: Directive,
            args: [{
                    selector: 'input[manualDisabled], textarea[manualDisabled]',
                }]
        }], propDecorators: { manualDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "manualDisabled", required: false }] }] } });

class ManualNameDirective {
    constructor() {
        this.el = inject(ElementRef);
        // Bind as: manualName="email" or [manualName]="expr"
        this.manualName = input(null, { ...(ngDevMode ? { debugName: "manualName" } : {}), alias: 'manualName' });
        this.syncNameEffect = effect(() => {
            const name = this.manualName();
            if (name == null)
                return;
            const native = this.el.nativeElement;
            if (!native)
                return;
            if (native.name !== name) {
                native.name = name;
            }
        }, ...(ngDevMode ? [{ debugName: "syncNameEffect" }] : []));
    }
    static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: ManualNameDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
    static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.1.0", type: ManualNameDirective, isStandalone: true, selector: "input[manualName], textarea[manualName]", inputs: { manualName: { classPropertyName: "manualName", publicName: "manualName", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: ManualNameDirective, decorators: [{
            type: Directive,
            args: [{
                    selector: 'input[manualName], textarea[manualName]',
                }]
        }], propDecorators: { manualName: [{ type: i0.Input, args: [{ isSignal: true, alias: "manualName", required: false }] }] } });

class ManualReadonlyDirective {
    constructor() {
        this.el = inject(ElementRef);
        // Bind as: [manualReadonly]="true"
        this.manualReadonly = input(null, { ...(ngDevMode ? { debugName: "manualReadonly" } : {}), alias: 'manualReadonly' });
        this.syncReadonlyEffect = effect(() => {
            const readonly = this.manualReadonly();
            if (readonly == null)
                return;
            const native = this.el.nativeElement;
            if (!native)
                return;
            native.readOnly = !!readonly;
        }, ...(ngDevMode ? [{ debugName: "syncReadonlyEffect" }] : []));
    }
    static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: ManualReadonlyDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
    static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.1.0", type: ManualReadonlyDirective, isStandalone: true, selector: "input[manualReadonly], textarea[manualReadonly]", inputs: { manualReadonly: { classPropertyName: "manualReadonly", publicName: "manualReadonly", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: ManualReadonlyDirective, decorators: [{
            type: Directive,
            args: [{
                    selector: 'input[manualReadonly], textarea[manualReadonly]',
                }]
        }], propDecorators: { manualReadonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "manualReadonly", required: false }] }] } });

class ManualTypeDirective {
    constructor() {
        this.el = inject(ElementRef);
        // Bind as: manualType="password" or [manualType]="expr"
        this.manualType = input(null, { ...(ngDevMode ? { debugName: "manualType" } : {}), alias: 'manualType' });
        this.syncTypeEffect = effect(() => {
            const t = this.manualType();
            if (!t)
                return;
            const native = this.el.nativeElement;
            if (!native)
                return;
            if (native.type !== t) {
                native.type = t;
            }
        }, ...(ngDevMode ? [{ debugName: "syncTypeEffect" }] : []));
    }
    static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: ManualTypeDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
    static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.1.0", type: ManualTypeDirective, isStandalone: true, selector: "input[manualType], textarea[manualType]", inputs: { manualType: { classPropertyName: "manualType", publicName: "manualType", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: ManualTypeDirective, decorators: [{
            type: Directive,
            args: [{
                    selector: 'input[manualType], textarea[manualType]',
                }]
        }], propDecorators: { manualType: [{ type: i0.Input, args: [{ isSignal: true, alias: "manualType", required: false }] }] } });

class ArrPipe {
    transform(data, type, refresh) {
        if (!data) {
            return [];
        }
        if (typeof data == 'string')
            return data.split(type || ' ');
        if (Array.isArray(data)) {
            return data;
        }
        if (typeof data != 'object') {
            return [];
        }
        let arr = [];
        for (let each in data) {
            if (!data[each])
                continue;
            if (type == 'prop') {
                arr.push(each);
            }
            else if (type == 'value') {
                arr.push(data[each]);
            }
            else {
                arr.push({
                    prop: each,
                    value: data[each],
                });
            }
        }
        return arr;
    }
    static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: ArrPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
    static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.0", ngImport: i0, type: ArrPipe, isStandalone: true, name: "arr" }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: ArrPipe, decorators: [{
            type: Pipe,
            args: [{
                    name: 'arr',
                }]
        }] });

class MongodatePipe {
    transform(_id) {
        if (!_id)
            return new Date();
        let timestamp = _id.toString().substring(0, 8);
        return new Date(parseInt(timestamp, 16) * 1000);
    }
    static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: MongodatePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
    static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.0", ngImport: i0, type: MongodatePipe, isStandalone: true, name: "mongodate" }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: MongodatePipe, decorators: [{
            type: Pipe,
            args: [{
                    name: 'mongodate',
                }]
        }] });

class NumberPipe {
    transform(value) {
        const result = Number(value); // Convert value to a number
        return isNaN(result) ? 0 : result; // Return 0 if conversion fails
    }
    static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: NumberPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
    static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.0", ngImport: i0, type: NumberPipe, isStandalone: true, name: "number" }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: NumberPipe, decorators: [{
            type: Pipe,
            args: [{
                    name: 'number',
                }]
        }] });

class PaginationPipe {
    transform(arr, config, sort, search = '') {
        if (!Array.isArray(arr))
            return [];
        arr = arr.slice();
        for (let i = 0; i < arr.length; i++) {
            arr[i].num = i + 1;
        }
        if (sort.direction) {
            arr.sort((a, b) => {
                if (a[sort.title] < b[sort.title]) {
                    return sort.direction == 'desc' ? 1 : -1;
                }
                if (a[sort.title] > b[sort.title]) {
                    return sort.direction == 'desc' ? -1 : 1;
                }
                return 0;
            });
        }
        return arr.slice((config.page - 1) * config.perPage, config.page * config.perPage);
    }
    static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: PaginationPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
    static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.0", ngImport: i0, type: PaginationPipe, isStandalone: true, name: "page", pure: false }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: PaginationPipe, decorators: [{
            type: Pipe,
            args: [{
                    name: 'page',
                    pure: false,
                }]
        }] });

class SafePipe {
    constructor() {
        this._sanitizer = inject(DomSanitizer);
    }
    transform(html) {
        return this._sanitizer.bypassSecurityTrustResourceUrl(html);
    }
    static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: SafePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
    static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.0", ngImport: i0, type: SafePipe, isStandalone: true, name: "safe" }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: SafePipe, decorators: [{
            type: Pipe,
            args: [{
                    name: 'safe',
                }]
        }] });

class SearchPipe {
    transform(items, query, fields, limit, ignore = false, _reload) {
        /* unwrap signals */
        const q = isSignal(query) ? query() : query;
        let f = isSignal(fields) ? fields() : fields;
        /* allow “fields” to be a number (=limit) */
        if (typeof f === 'number') {
            limit = f;
            f = undefined;
        }
        const docs = Array.isArray(items) ? items : Object.values(items);
        if (ignore || !q)
            return limit ? docs.slice(0, limit) : docs;
        /* normalise fields */
        const paths = !f
            ? ['name']
            : Array.isArray(f)
                ? f
                : f.trim().split(/\s+/);
        /* normalise query */
        const needles = Array.isArray(q)
            ? q.map((s) => s.toLowerCase())
            : typeof q === 'object'
                ? Object.keys(q)
                    .filter((k) => q[k])
                    .map((k) => k.toLowerCase())
                : [q.toLowerCase()];
        const txtMatches = (val) => {
            if (val == null)
                return false;
            const hay = val.toString().toLowerCase();
            return needles.some((n) => hay.includes(n) || n.includes(hay));
        };
        const walk = (obj, parts) => {
            if (!obj)
                return false;
            const [head, ...rest] = parts;
            const next = obj[head];
            if (Array.isArray(next))
                return next.some((v) => rest.length ? walk(v, rest) : txtMatches(v));
            return rest.length ? walk(next, rest) : txtMatches(next);
        };
        const out = [];
        const seen = new Set();
        const check = (doc, key) => {
            for (const p of paths) {
                if (walk(doc, p.split('.'))) {
                    if (!seen.has(key)) {
                        out.push(doc);
                        seen.add(key);
                    }
                    break;
                }
            }
        };
        Array.isArray(items)
            ? docs.forEach((d, i) => check(d, i))
            : Object.entries(items).forEach(([k, v]) => check(v, k));
        return limit ? out.slice(0, limit) : out;
    }
    static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: SearchPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
    static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.0", ngImport: i0, type: SearchPipe, isStandalone: true, name: "search" }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: SearchPipe, decorators: [{
            type: Pipe,
            args: [{ name: 'search', pure: true }]
        }] });

class SplicePipe {
    transform(from, which, refresh) {
        if (Array.isArray(from))
            from = { arr: from, prop: '_id' };
        let arr = (which.keep && []) || from.arr.slice();
        if (Array.isArray(which))
            which = { arr: which, prop: '_id' };
        for (let i = from.arr.length - 1; i >= 0; i--) {
            for (let j = 0; j < which.arr.length; j++) {
                if (from.prop && which.prop) {
                    if (from.arr[i][from.prop] == which.arr[j][which.prop]) {
                        if (which.keep) {
                            arr.push(from.arr[i]);
                        }
                        else {
                            arr.splice(i, 1);
                        }
                        break;
                    }
                }
                else if (from.prop) {
                    if (from.arr[i][from.prop] == which.arr[j]) {
                        if (which.keep) {
                            arr.push(from.arr[i]);
                        }
                        else {
                            arr.splice(i, 1);
                        }
                        break;
                    }
                }
                else if (which.prop) {
                    if (from.arr[i] == which.arr[j][which.prop]) {
                        if (which.keep) {
                            arr.push(from.arr[i]);
                        }
                        else {
                            arr.splice(i, 1);
                        }
                        break;
                    }
                }
                else if (from.arr[i] == which.arr[j]) {
                    if (which.keep) {
                        arr.push(from.arr[i]);
                    }
                    else {
                        arr.splice(i, 1);
                    }
                    break;
                }
            }
        }
        return arr;
    }
    static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: SplicePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
    static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.0", ngImport: i0, type: SplicePipe, isStandalone: true, name: "splice" }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: SplicePipe, decorators: [{
            type: Pipe,
            args: [{
                    name: 'splice',
                }]
        }] });

class SplitPipe {
    transform(value, index = 0, devider = ':') {
        const arr = value.split(devider);
        return arr.length > index ? arr[index] : '';
    }
    static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: SplitPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
    static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.0", ngImport: i0, type: SplitPipe, isStandalone: true, name: "split" }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: SplitPipe, decorators: [{
            type: Pipe,
            args: [{
                    name: 'split',
                }]
        }] });

class EmitterService {
    constructor() {
        this._signals = new Map();
        this._closers = new Map();
        this._streams = new Map();
        this._done = new Map();
    }
    _getSignal(id) {
        let s = this._signals.get(id);
        if (!s) {
            // emit even if same payload repeats
            s = signal(undefined, { equal: () => false });
            this._signals.set(id, s);
        }
        return s;
    }
    _getCloser(id) {
        let c = this._closers.get(id);
        if (!c) {
            c = new Subject();
            this._closers.set(id, c);
        }
        return c;
    }
    _getStream(id) {
        let obs$ = this._streams.get(id);
        if (!obs$) {
            const sig = this._getSignal(id);
            const closed$ = this._getCloser(id);
            obs$ = toObservable(sig).pipe(
            // Subject-like: don't replay the current value on subscribe
            skip(1), takeUntil(closed$), share());
            this._streams.set(id, obs$);
        }
        return obs$;
    }
    /** Emit an event */
    emit(id, data) {
        this._getSignal(id).set(data);
    }
    /** Listen for events (hot, completes when off(id) is called) */
    on(id) {
        return this._getStream(id);
    }
    /** Complete and remove a channel */
    off(id) {
        const closer = this._closers.get(id);
        if (closer) {
            closer.next();
            closer.complete();
            this._closers.delete(id);
        }
        this._signals.delete(id);
        this._streams.delete(id);
    }
    offAll() {
        for (const id of Array.from(this._closers.keys()))
            this.off(id);
    }
    has(id) {
        return this._signals.has(id);
    }
    _getDoneSignal(id) {
        let s = this._done.get(id);
        if (!s) {
            s = signal(undefined);
            this._done.set(id, s);
        }
        return s;
    }
    /** Mark task as completed with a payload (default: true) */
    complete(task, value = true) {
        this._getDoneSignal(task).set(value);
    }
    /** Clear completion so it can be awaited again */
    clearCompleted(task) {
        const s = this._done.get(task) ?? this._getDoneSignal(task);
        s.set(undefined);
    }
    /** Read current completion payload (undefined => not completed) */
    completed(task) {
        return this._getDoneSignal(task)();
    }
    isCompleted(task) {
        return this._getDoneSignal(task)() !== undefined;
    }
    onComplete(tasks, opts) {
        const list = (Array.isArray(tasks) ? tasks : [tasks]).filter(Boolean);
        const streams = list.map((id) => toObservable(this._getDoneSignal(id)).pipe(filter((v) => v !== undefined), map((v) => v)));
        let source$;
        if (list.length <= 1) {
            // single-task await
            source$ = streams[0]?.pipe(take(1)) ?? new Observable();
        }
        else if (opts?.mode === 'any') {
            source$ = merge(...streams).pipe(take(1));
        }
        else {
            source$ = combineLatest(streams).pipe(take(1));
        }
        if (opts?.timeoutMs && Number.isFinite(opts.timeoutMs)) {
            source$ = source$.pipe(timeout({ first: opts.timeoutMs }));
        }
        if (opts?.abort) {
            const abort$ = new Observable((sub) => {
                const handler = () => {
                    sub.next();
                    sub.complete();
                };
                opts.abort.addEventListener('abort', handler);
                return () => opts.abort.removeEventListener('abort', handler);
            });
            source$ = source$.pipe(takeUntil(abort$));
        }
        return source$;
    }
    static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: EmitterService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
    static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: EmitterService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: EmitterService, decorators: [{
            type: Injectable,
            args: [{ providedIn: 'root' }]
        }] });

class HttpService {
    constructor(config, _http) {
        this._http = _http;
        // An array of error handling callbacks
        this.errors = [];
        // Base URL for HTTP requests
        this.url = localStorage.getItem('wacom-http.url') || '';
        // Flag to lock the service to prevent multiple requests
        this.locked = false;
        // Array to store setTimeout IDs for managing request locks
        this.awaitLocked = [];
        // Object to store HTTP headers
        this._headers = localStorage.getItem('wacom-http.headers')
            ? JSON.parse(localStorage.getItem('wacom-http.headers'))
            : {};
        // Instance of HttpHeaders with current headers
        this._http_headers = new HttpHeaders(this._headers);
        // Initialize HTTP configuration and headers from injected config
        this._config = {
            ...DEFAULT_HTTP_CONFIG,
            ...(config.http || {}),
        };
        if (typeof this._config.url === 'string') {
            this.setUrl(this._config.url);
        }
        if (typeof this._config.headers === 'object') {
            for (const header in this._config.headers) {
                this._headers[header] = this._config.headers[header];
            }
            this._http_headers = new HttpHeaders(this._headers);
        }
    }
    // Set a new base URL and save it in the store
    setUrl(url) {
        this.url = url;
        localStorage.setItem('wacom-http.url', url);
    }
    // Remove the base URL and revert to the default or stored one
    removeUrl() {
        this.url = this._config.url || '';
        localStorage.removeItem('wacom-http.url');
    }
    // Set a new HTTP header and update the stored headers
    set(key, value) {
        this._headers[key] = value;
        localStorage.setItem('wacom-http.headers', JSON.stringify(this._headers));
        this._http_headers = new HttpHeaders(this._headers);
    }
    // Get the value of a specific HTTP header
    header(key) {
        return this._headers[key];
    }
    // Remove a specific HTTP header and update the stored headers
    remove(key) {
        delete this._headers[key];
        localStorage.setItem('wacom-http.headers', JSON.stringify(this._headers));
        this._http_headers = new HttpHeaders(this._headers);
    }
    // Internal method to make HTTP requests based on the method type
    _httpMethod(method, _url, doc, headers) {
        if (method === 'post') {
            return this._http.post(_url, doc, headers);
        }
        else if (method === 'put') {
            return this._http.put(_url, doc, headers);
        }
        else if (method === 'patch') {
            return this._http.patch(_url, doc, headers);
        }
        else if (method === 'delete') {
            return this._http.delete(_url, headers);
        }
        else {
            return this._http.get(_url, headers);
        }
    }
    /**
     * Internal method to handle HTTP requests for various methods (POST, PUT, PATCH, DELETE, GET).
     *
     * Features:
     * - **Request Locking**: Manages request locking to prevent simultaneous requests.
     * - **Acceptance Check**: Validates the server response against a user-defined `acceptance` function.
     *   If the check fails, the response is rejected with an error.
     * - **Replace Logic**: Allows modification of specific parts of the response object, determined by a user-defined `replace` function.
     *   Can handle both objects and arrays within the response.
     * - **Field Filtering**: Supports extracting specific fields from response objects or arrays.
     * - **Legacy Support**: Compatible with callback-based usage alongside Observables.
     * - **ReplaySubject**: Ensures that the response can be shared across multiple subscribers.
     *
     * @param url - The endpoint to send the HTTP request to (relative to the base URL).
     * @param doc - The request payload for methods like POST, PUT, and PATCH.
     * @param callback - A legacy callback function to handle the response.
     * @param opts - Additional options:
     *   - `err`: Error handling callback.
     *   - `acceptance`: Function to validate the server response. Should return `true` for valid responses.
     *   - `replace`: Function to modify specific parts of the response data.
     *   - `fields`: Array of fields to extract from the response object(s).
     *   - `data`: Path in the response where the data resides for `replace` and `fields` operations.
     *   - `skipLock`: If `true`, bypasses request locking.
     *   - `url`: Overrides the base URL for this request.
     * @param method - The HTTP method (e.g., 'post', 'put', 'patch', 'delete', 'get').
     * @returns An Observable that emits the processed HTTP response or an error.
     */
    _post(url, doc, callback = (resp) => { }, opts = {}, method = 'post') {
        if (typeof opts === 'function') {
            opts = { err: opts };
        }
        if (!opts.err) {
            opts.err = (err) => { };
        }
        // Handle request locking to avoid multiple simultaneous requests
        if (this.locked && !opts.skipLock) {
            return new Observable((observer) => {
                const wait = setTimeout(() => {
                    this._post(url, doc, callback, opts, method).subscribe(observer);
                }, 100);
                this.awaitLocked.push(wait);
            });
        }
        const _url = (opts.url || this.url) + url;
        this.prepare_handle(_url, doc);
        // Using ReplaySubject to allow multiple subscriptions without re-triggering the HTTP request
        const responseSubject = new ReplaySubject(1);
        this._httpMethod(method, _url, doc, { headers: this._http_headers })
            .pipe(first(), catchError((error) => {
            this.handleError(opts.err, () => {
                this._post(url, doc, callback, opts, method).subscribe(responseSubject);
            })(error);
            responseSubject.error(error);
            return EMPTY;
        }))
            .subscribe({
            next: (resp) => {
                if (opts.acceptance &&
                    typeof opts.acceptance === 'function') {
                    if (!opts.acceptance(resp)) {
                        const error = new HttpErrorResponse({
                            error: 'Acceptance failed',
                            status: 400,
                        });
                        this.handleError(opts.err, () => { })(error);
                        responseSubject.error(error);
                        return;
                    }
                }
                if (opts.replace && typeof opts.replace === 'function') {
                    if (Array.isArray(this._getObjectToReplace(resp, opts.data))) {
                        this._getObjectToReplace(resp, opts.data).map((item) => opts.replace(item));
                    }
                    else if (this._getObjectToReplace(resp, opts.data)) {
                        opts.replace(this._getObjectToReplace(resp, opts.data));
                    }
                }
                if (Array.isArray(opts.fields)) {
                    if (Array.isArray(this._getObjectToReplace(resp, opts.data))) {
                        this._getObjectToReplace(resp, opts.data).map((item) => {
                            return this._newDoc(item, opts.fields);
                        });
                    }
                    else if (this._getObjectToReplace(resp, opts.data)) {
                        const newDoc = this._newDoc(this._getObjectToReplace(resp, opts.data), opts.fields);
                        if (opts.data) {
                            this._setObjectToReplace(resp, opts.data, newDoc);
                        }
                        else {
                            resp = newDoc;
                        }
                    }
                }
                this.response_handle(_url, resp, () => callback(resp));
                responseSubject.next(resp);
                responseSubject.complete();
            },
            error: (err) => responseSubject.error(err),
            complete: () => responseSubject.complete(),
        });
        return responseSubject.asObservable();
    }
    /**
     * Public method to perform a POST request.
     * - Supports legacy callback usage.
     * - Returns an Observable for reactive programming.
     */
    post(url, doc, callback = (resp) => { }, opts = {}) {
        return this._post(url, doc, callback, opts);
    }
    /**
     * Public method to perform a PUT request.
     * - Supports legacy callback usage.
     * - Returns an Observable for reactive programming.
     */
    put(url, doc, callback = (resp) => { }, opts = {}) {
        return this._post(url, doc, callback, opts, 'put');
    }
    /**
     * Public method to perform a PATCH request.
     * - Supports legacy callback usage.
     * - Returns an Observable for reactive programming.
     */
    patch(url, doc, callback = (resp) => { }, opts = {}) {
        return this._post(url, doc, callback, opts, 'patch');
    }
    /**
     * Public method to perform a DELETE request.
     * - Supports legacy callback usage.
     * - Returns an Observable for reactive programming.
     */
    delete(url, callback = (resp) => { }, opts = {}) {
        return this._post(url, null, callback, opts, 'delete');
    }
    /**
     * Public method to perform a GET request.
     * - Supports legacy callback usage.
     * - Returns an Observable for reactive programming.
     */
    get(url, callback = (resp) => { }, opts = {}) {
        return this._post(url, null, callback, opts, 'get');
    }
    // Clear all pending request locks
    clearLocked() {
        for (const awaitLocked of this.awaitLocked) {
            clearTimeout(awaitLocked);
        }
        this.awaitLocked = [];
    }
    // Lock the service to prevent multiple simultaneous requests
    lock() {
        this.locked = true;
    }
    // Unlock the service to allow new requests
    unlock() {
        this.locked = false;
    }
    /**
     * Handles HTTP errors.
     * - Calls provided error callback and retries the request if needed.
     */
    handleError(callback, retry) {
        return (error) => {
            return new Promise((resolve) => {
                this.err_handle(error, callback, retry);
                resolve();
            });
        };
    }
    /**
     * Internal method to trigger error handling callbacks.
     */
    err_handle(err, next, retry) {
        if (typeof next === 'function') {
            next(err);
        }
        for (const callback of this.errors) {
            if (typeof callback === 'function') {
                callback(err, retry);
            }
        }
    }
    // Placeholder method for handling request preparation (can be customized)
    prepare_handle(url, body) { }
    // Placeholder method for handling the response (can be customized)
    response_handle(url, body, next) {
        if (typeof next === 'function') {
            next();
        }
    }
    /**
     * Retrieves a nested object or property from the response based on a dot-separated path.
     *
     * @param resp - The response object to retrieve data from.
     * @param base - A dot-separated string indicating the path to the desired property within the response.
     *   - Example: `'data.items'` will navigate through `resp.data.items`.
     *   - If empty, the entire response is returned.
     * @returns The object or property located at the specified path within the response.
     */
    _getObjectToReplace(resp, base = '') {
        if (base.includes('.')) {
            const newBase = base.split('');
            const currentBase = newBase.pop() || '';
            return this._getObjectToReplace(resp[currentBase] || {}, newBase.join('.'));
        }
        else if (base) {
            return resp[base];
        }
        else {
            return resp;
        }
    }
    /**
     * Sets or replaces a nested object or property in the response based on a dot-separated path.
     *
     * @param resp - The response object to modify.
     * @param base - A dot-separated string indicating the path to the property to replace.
     *   - Example: `'data.items'` will navigate through `resp.data.items`.
     * @param doc - The new data or object to set at the specified path.
     * @returns `void`.
     */
    _setObjectToReplace(resp, base = '', doc) {
        while (base.includes('.')) {
            const newBase = base.split('');
            const currentBase = newBase.pop() || '';
            resp = resp[currentBase] || {};
            base = newBase.join('.');
        }
        resp[base] = doc;
    }
    /**
     * Creates a new object containing only specified fields from the input item.
     *
     * @param item - The input object to extract fields from.
     * @param fields - An array of field names to include in the new object.
     *   - Example: `['id', 'name']` will create a new object with only the `id` and `name` properties from `item`.
     * @returns A new object containing only the specified fields.
     */
    _newDoc(item, fields) {
        const newDoc = {};
        for (const field of fields) {
            newDoc[field] = item[field];
        }
        return newDoc;
    }
    static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: HttpService, deps: [{ token: CONFIG_TOKEN, optional: true }, { token: i1$1.HttpClient }], target: i0.ɵɵFactoryTarget.Injectable }); }
    static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: HttpService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: HttpService, decorators: [{
            type: Injectable,
            args: [{
                    providedIn: 'root',
                }]
        }], ctorParameters: () => [{ type: undefined, decorators: [{
                    type: Inject,
                    args: [CONFIG_TOKEN]
                }, {
                    type: Optional
                }] }, { type: i1$1.HttpClient }] });

// network.service.ts — Angular 20+ (zoneless) signal-based connectivity checker
class NetworkService {
    /**
     * Creates the network monitor, binds browser/Capacitor events,
     * performs an immediate check, and starts periodic polling.
     */
    constructor(config) {
        /** Internal mutable signals. */
        this._status = signal(navigator.onLine ? 'poor' : 'none', ...(ngDevMode ? [{ debugName: "_status" }] : []));
        this._latencyMs = signal(null, ...(ngDevMode ? [{ debugName: "_latencyMs" }] : []));
        this._isOnline = signal(navigator.onLine, ...(ngDevMode ? [{ debugName: "_isOnline" }] : []));
        /** Public read-only signals. */
        this.status = this._status.asReadonly();
        this.latencyMs = this._latencyMs.asReadonly();
        this.isOnline = this._isOnline.asReadonly();
        /** Failure counter to decide "none". */
        this.fails = 0;
        this._emitterService = inject(EmitterService);
        this._config = {
            ...DEFAULT_NETWORK_CONFIG,
            ...(config.network || {}),
        };
        this._bindEvents();
        this.recheckNow(); // fire once on start
        window.setInterval(() => this.recheckNow(), this._config.intervalMs);
    }
    /**
     * Manually trigger a connectivity check.
     * - Measures latency against the first reachable endpoint.
     * - Updates `isOnline`, `latencyMs`, and `status` accordingly.
     */
    async recheckNow() {
        const res = await this._pingAny();
        if (res.ok && res.latency != null) {
            this.fails = 0;
            this._latencyMs.set(res.latency);
            this._isOnline.set(true);
        }
        else {
            this.fails++;
            this._latencyMs.set(null);
            // `isOnline` may still be true per OS; we let online/offline events keep it in sync.
        }
        this._updateClassification();
    }
    // ─────────────────────────── Internals ───────────────────────────
    /**
     * Classifies current state into 'good' | 'poor' | 'none'.
     * - 'none' if offline or too many consecutive failures.
     * - 'good' if latency ≤ goodLatencyMs.
     * - otherwise 'poor'.
     */
    _updateClassification() {
        if (!this._isOnline() ||
            this.fails >= this._config.maxConsecutiveFails) {
            if (this._status() !== 'none') {
                this._status.set('none');
                this._emitterService.emit('wacom_offline');
            }
            return;
        }
        const l = this._latencyMs();
        if (l != null && l <= this._config.goodLatencyMs) {
            if (this._status() !== 'good') {
                this._status.set('good');
                this._emitterService.emit('wacom_online');
            }
        }
        else if (this._status() !== 'poor') {
            this._status.set('poor');
            this._emitterService.emit('wacom_online');
        }
    }
    /**
     * Binds browser events that can affect connectivity:
     * - online/offline (OS connectivity)
     * - visibilitychange (recheck on focus)
     * - NetworkInformation 'change' (if supported)
     */
    _bindEvents() {
        window.addEventListener('online', () => {
            this._isOnline.set(true);
            this.recheckNow();
        });
        window.addEventListener('offline', () => {
            this._isOnline.set(false);
            this._updateClassification();
        });
        navigator.connection?.addEventListener?.('change', () => this.recheckNow());
    }
    /**
     * Tries endpoints in order until one responds (CORS or opaque).
     * Returns success with measured latency, or a failure result.
     */
    async _pingAny() {
        for (const url of this._config.endpoints) {
            const noCors = !url.includes('api.webart.work'); // treat public fallbacks as opaque checks
            const r = await this._measure(url, this._config.timeoutMs, noCors).catch(() => null);
            if (r?.ok)
                return r;
        }
        return { ok: false, latency: null };
    }
    /**
     * Measures a single fetch:
     * - Appends a timestamp to bypass caches.
     * - Uses `no-store` to avoid intermediaries caching.
     * - When `noCors` is true, uses `mode:'no-cors'` and treats a resolved fetch as reachable.
     */
    async _measure(url, timeoutMs, noCors = false) {
        const ctrl = new AbortController();
        const timer = setTimeout(() => ctrl.abort(), timeoutMs);
        const t0 = performance.now();
        try {
            const res = await fetch(url + (url.includes('?') ? '&' : '?') + 't=' + Date.now(), {
                method: 'GET',
                cache: 'no-store',
                credentials: 'omit',
                signal: ctrl.signal,
                mode: noCors ? 'no-cors' : 'cors',
            });
            clearTimeout(timer);
            const latency = Math.round(performance.now() - t0);
            // In no-cors, Response is opaque; treat as success if the fetch resolved.
            const ok = noCors ? true : res.ok;
            return { ok, latency };
        }
        catch {
            clearTimeout(timer);
            return { ok: false, latency: null };
        }
    }
    static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: NetworkService, deps: [{ token: CONFIG_TOKEN, optional: true }], target: i0.ɵɵFactoryTarget.Injectable }); }
    static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: NetworkService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: NetworkService, decorators: [{
            type: Injectable,
            args: [{ providedIn: 'root' }]
        }], ctorParameters: () => [{ type: undefined, decorators: [{
                    type: Inject,
                    args: [CONFIG_TOKEN]
                }, {
                    type: Optional
                }] }] });

class StoreService {
    constructor(config) {
        this._prefix = '';
        this._config = {
            ...DEFAULT_CONFIG,
            ...(config?.store || {}),
        };
    }
    /**
     * Sets the prefix for storage keys.
     *
     * @param prefix - The prefix to set.
     */
    setPrefix(prefix) {
        this._prefix = prefix;
    }
    /**
     * Sets a value in storage asynchronously.
     *
     * @param key - The storage key.
     * @param value - The value to store.
     * @returns A promise that resolves to a boolean indicating success.
     */
    async set(key, value, callback = () => { }, errCallback = () => { }) {
        key = this._applyPrefix(key);
        try {
            if (this._config.set) {
                await this._config.set(key, value, callback, errCallback);
            }
            else {
                localStorage.setItem(key, value);
                callback();
            }
            return true;
        }
        catch (err) {
            console.error(err);
            errCallback(err);
            return false;
        }
    }
    /**
     * Gets a value from storage asynchronously.
     *
     * @param key - The storage key.
     * @returns A promise that resolves to the retrieved value or `null` if the key is missing.
     */
    async get(key, callback, errCallback = () => { }) {
        key = this._applyPrefix(key);
        try {
            if (this._config.get) {
                const value = await this._config.get(key, (val) => {
                    callback?.(val ?? null);
                }, errCallback);
                return value ?? null;
            }
            else {
                const value = localStorage.getItem(key);
                callback?.(value ?? null);
                return value ?? null;
            }
        }
        catch (err) {
            console.error(err);
            errCallback(err);
            return null;
        }
    }
    /**
     * Sets a JSON value in storage asynchronously.
     *
     * @param key - The storage key.
     * @param value - The value to store.
     * @returns A promise that resolves to a boolean indicating success.
     */
    async setJson(key, value, callback = () => { }, errCallback = () => { }) {
        return await this.set(key, JSON.stringify(value), callback, errCallback);
    }
    /**
     * Gets a JSON value from storage asynchronously.
     *
     * @param key - The storage key.
     * @returns A promise that resolves to the retrieved value.
     */
    async getJson(key, callback, errCallback = () => { }) {
        const value = await this.get(key);
        if (value === null) {
            return null;
        }
        try {
            const result = JSON.parse(value);
            callback?.(result);
            return result;
        }
        catch (err) {
            errCallback?.(err);
            console.error(err);
            return null;
        }
    }
    /**
     * Removes a value from storage.
     *
     * @param key - The storage key.
     * @param callback - The callback to execute on success.
     * @param errCallback - The callback to execute on error.
     * @returns A promise that resolves to a boolean indicating success.
     */
    async remove(key, callback = () => { }, errCallback = () => { }) {
        key = this._applyPrefix(key);
        try {
            if (this._config.remove) {
                return await this._config.remove(key, callback, errCallback);
            }
            else {
                localStorage.removeItem(key);
                callback();
                return true;
            }
        }
        catch (err) {
            console.error(err);
            errCallback(err);
            return false;
        }
    }
    /**
     * Clears all values from storage.
     *
     * @param callback - The callback to execute on success.
     * @param errCallback - The callback to execute on error.
     * @returns A promise that resolves to a boolean indicating success.
     */
    async clear(callback, errCallback) {
        try {
            if (this._config.clear) {
                await this._config.clear();
            }
            else {
                localStorage.clear();
            }
            callback?.();
            return true;
        }
        catch (err) {
            console.error(err);
            errCallback?.(err);
            return false;
        }
    }
    /**
     * Applies the configured prefix to a storage key.
     *
     * @param key - The storage key.
     * @returns The prefixed storage key.
     */
    _applyPrefix(key) {
        if (this._config.prefix) {
            key = this._config.prefix + key;
        }
        if (this._prefix) {
            key = this._prefix + key;
        }
        return key;
    }
    static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: StoreService, deps: [{ token: CONFIG_TOKEN, optional: true }], target: i0.ɵɵFactoryTarget.Injectable }); }
    static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: StoreService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: StoreService, decorators: [{
            type: Injectable,
            args: [{
                    providedIn: 'root',
                }]
        }], ctorParameters: () => [{ type: undefined, decorators: [{
                    type: Inject,
                    args: [CONFIG_TOKEN]
                }, {
                    type: Optional
                }] }] });

/**
 * Abstract class representing a CRUD (Create, Read, Update, Delete) service.
 *
 * This class provides methods for managing documents, interacting with an API,
 * and storing/retrieving data from local storage. It is designed to be extended
 * for specific document types.
 *
 * @template Document - The type of the document the service handles.
 */
class CrudService {
    constructor(_config) {
        this._config = _config;
        /**
         * Base URL for the API collection associated with this service.
         */
        this._url = '/api/';
        /**
         * In-memory cache with all documents currently known by the service.
         */
        this._docs = [];
        /**
         * Number of documents per page for paginated `get()` calls.
         */
        this._perPage = 20;
        /**
         * Registered callbacks that recompute filtered document views.
         */
        this._filteredDocumentsCallbacks = [];
        /**
         * HTTP client wrapper used for API communication.
         */
        this.__httpService = inject(HttpService);
        /**
         * Key–value storage service used to persist documents locally.
         */
        this.__storeService = inject(StoreService);
        /**
         * Core helper service with utility methods (copy, debounce, toSignal, etc.).
         */
        this.__coreService = inject(CoreService);
        /**
         * Global event bus for cross-service communication.
         */
        this.__emitterService = inject(EmitterService);
        /**
         * Network status service used to queue work while offline.
         */
        this.__networkService = inject(NetworkService);
        /**
         * Cache of per-document signals indexed by document _id.
         * Prevents creating multiple signals for the same document.
         */
        this._signal = {};
        /**
         * Cache of per (field,value) lists of document signals.
         * Key format: `${field}_${JSON.stringify(value)}`.
         */
        this._signals = {};
        /**
         * Cache of per-field maps: fieldValue -> array of document signals.
         */
        this._fieldSignals = {};
        /**
         * Track pending fetch-by-id requests to avoid duplicate calls.
         */
        this._fetchingId = {};
        /**
         * Queue of operations that must be retried when network comes back online.
         */
        this._onOnline = [];
        /**
         * Local counter used to build unique local identifiers together with Date.now().
         */
        this._randomCount = 0;
        this._config.signalFields = this._config.signalFields || {};
        this._url += this._config.name;
        this.loaded = this.__emitterService.onComplete(this._config.name + '_loaded');
        this.getted = this.__emitterService.onComplete(this._config.name + '_getted');
        if (this._config.unauthorized) {
            this.restoreDocs();
        }
        else if (localStorage.getItem('waw_user')) {
            const user = JSON.parse(localStorage.getItem('waw_user'));
            if (user._id ===
                localStorage.getItem(this._config.name + 'waw_user_id')) {
                this.restoreDocs();
            }
        }
        this.__emitterService.on('wipe').subscribe(() => {
            this.clearDocs();
            this._filterDocuments();
        });
        this.__emitterService.on('wacom_online').subscribe(() => {
            for (const callback of this._onOnline) {
                callback();
            }
            this._onOnline.length = 0;
        });
    }
    /**
     * Returns a WritableSignal for a document by _id, creating it if absent.
     * Caches the signal to avoid redundant instances and initializes it
     * with the current snapshot of the document.
     * Work very carefully with this and localId, better avoid such flows.
     *
     * @param _id - Document identifier or a document instance.
     */
    getSignal(_id) {
        if (typeof _id !== 'string') {
            _id = this._id(_id);
        }
        // Reuse existing signal if present
        if (this._signal[_id]) {
            return this._signal[_id];
        }
        // Always base the signal on the current canonical doc()
        const doc = this.doc(_id);
        this._signal[_id] = this.__coreService.toSignal(doc, this._config.signalFields);
        return this._signal[_id];
    }
    /**
     * Returns a signal with an array of document signals that match
     * a given field/value pair.
     *
     * Example:
     *   const activitiesSig = service.getSignals('userId', currentUserId);
     */
    getSignals(field, value) {
        const id = field + '_' + JSON.stringify(value);
        if (!this._signals[id]) {
            this._signals[id] = signal(this._getSignals(id));
        }
        return this._signals[id];
    }
    /**
     * Builds the array of document signals for a given (field,value) key.
     * Only documents with a real _id are included.
     */
    _getSignals(id) {
        const sep = id.indexOf('_');
        if (sep === -1) {
            return [];
        }
        const field = id.slice(0, sep);
        const valueJson = id.slice(sep + 1);
        const list = [];
        for (const doc of this.getDocs()) {
            if (JSON.stringify(doc[field]) !== valueJson) {
                continue;
            }
            const docId = this._id(doc);
            if (!docId)
                continue;
            list.push(this.getSignal(docId));
        }
        return list;
    }
    /**
     * Returns a signal with a map: fieldValue -> array of document signals.
     *
     * Example:
     *   const byStatusSig = service.getFieldSignals('status');
     *   byStatusSig() might be { active: [sig1, sig2], draft: [sig3] }.
     */
    getFieldSignals(field) {
        if (!this._fieldSignals[field]) {
            this._fieldSignals[field] = signal(this._getFieldSignals(field));
        }
        return this._fieldSignals[field];
    }
    /**
     * Builds the map for a given field.
     * Only documents with a real _id are included.
     */
    _getFieldSignals(field) {
        const byFields = {};
        for (const doc of this.getDocs()) {
            const docId = this._id(doc);
            if (!docId)
                continue;
            const value = String(doc[field]);
            if (!byFields[value]) {
                byFields[value] = [];
            }
            byFields[value].push(this.getSignal(docId));
        }
        return byFields;
    }
    /**
     * Clears cached document signals except those explicitly preserved.
     * Useful when changing routes or contexts to reduce memory.
     *
     * @param exceptIds - List of ids whose signals should be kept.
     */
    removeSignals(exceptIds = []) {
        for (const _id in this._signal) {
            if (!exceptIds.includes(_id)) {
                delete this._signal[_id];
            }
        }
    }
    /**
     * Restores documents from local storage (if present) and syncs
     * all existing signals with the restored data.
     */
    async restoreDocs() {
        const docs = await this.__storeService.getJson('docs_' + this._config.name);
        if (docs?.length) {
            this._docs.length = 0;
            this._docs.push(...docs);
            this._filterDocuments();
            for (const doc of this._docs) {
                if (doc.__deleted) {
                    this.delete(doc, doc.__options?.['delete'] || {});
                }
                else if (!doc._id) {
                    this.create(doc, doc.__options?.['create'] || {});
                }
                else if (doc.__modified?.length) {
                    for (const id of doc.__modified) {
                        if (id.startsWith('up')) {
                            this.update(doc, doc.__options?.[id] || {});
                        }
                        else {
                            this.unique(doc, doc.__options?.[id] || {});
                        }
                    }
                }
            }
            this.__emitterService.complete(this._config.name + '_loaded', this._docs);
        }
    }
    /**
     * Saves the current set of documents to local storage.
     */
    setDocs() {
        this.__storeService.setJson('docs_' + this._config.name, this._docs);
    }
    /**
     * Retrieves the current list of documents.
     *
     * @returns The list of documents.
     */
    getDocs(filter = () => true) {
        return this._docs.filter(filter);
    }
    /**
     * Retrieves the first document that matches the given predicate.
     *
     * @param find - Predicate used to locate a specific document.
     */
    getDoc(find) {
        return this._docs.find(find);
    }
    /**
     * Clears the current list of documents, persists the empty state
     * and recomputes all derived signals.
     *
     * Empties the internal documents array and saves the updated state to local storage.
     */
    clearDocs() {
        this._docs.splice(0, this._docs.length);
        this.setDocs();
        this._updateSignals();
    }
    /**
     * Adds multiple documents to the service and saves them to local storage.
     *
     * @param docs - An array of documents to add.
     */
    addDocs(docs) {
        if (Array.isArray(docs)) {
            for (const doc of docs) {
                this.addDoc(doc);
            }
        }
    }
    /**
     * Adds a single document to the service. If it already exists, it will be updated.
     *
     * @param doc - The document to add.
     */
    addDoc(doc) {
        if (this._config.replace) {
            this._config.replace(doc);
        }
        const existingDoc = this._docs.find((d) => (this._id(doc) && this._id(d) === this._id(doc)) ||
            (doc._localId && d._localId === doc._localId));
        if (existingDoc) {
            this.__coreService.copy(doc, existingDoc);
            this.__coreService.copy(existingDoc, doc);
            this._syncSignalForDoc(existingDoc);
        }
        else {
            this._docs.push(doc);
            this._syncSignalForDoc(doc);
        }
        this.setDocs();
    }
    /**
     * Creates a new document with a temporary ID and status flags.
     *
     * @param doc - Optional base document to use for the new document.
     * @returns A new document instance with default properties.
     */
    new(doc = {}) {
        return {
            ...doc,
            _id: undefined,
            _localId: this._localId(),
            __created: false,
            __modified: false,
        };
    }
    /**
     * Retrieves a document by its unique ID or creates a new one if it doesn't exist.
     *
     * @param _id - The document ID to search for.
     * @returns The found document or a new document if not found.
     */
    doc(_id) {
        // If we already have a signal for this id, use its current value
        if (this._signal[_id]) {
            return this._signal[_id]();
        }
        let doc = this._docs.find((d) => this._id(d) === _id ||
            (d._localId && d._localId === Number(_id))) || null;
        // If doc not found, create + push into _docs so it is not detached
        if (!doc) {
            doc = this.new({ _id });
            this._docs.push(doc);
            this.setDocs();
        }
        if (!this._docs.find((d) => this._id(d) === _id) &&
            !this._fetchingId[_id]) {
            this._fetchingId[_id] = true;
            setTimeout(() => {
                this.fetch({ _id }).subscribe((_doc) => {
                    this._fetchingId[_id] = false;
                    if (_doc) {
                        this.__coreService.copy(_doc, doc);
                        this._syncSignalForDoc(doc);
                    }
                });
            });
        }
        return doc;
    }
    /**
     * Sets the number of documents to display per page.
     *
     * @param _perPage - Number of documents per page.
     */
    setPerPage(_perPage) {
        this._perPage = _perPage;
    }
    /**
     * Fetches a list of documents from the API with optional pagination.
     *
     * @param config - Optional pagination configuration.
     * @param options - Optional callback and error handling configuration.
     * @returns An observable that resolves with the list of documents.
     */
    get(config = {}, options = {}) {
        if (!this.__networkService.isOnline()) {
            return new Observable((observer) => {
                this._onOnline.push(() => {
                    this.get(config, options).subscribe(observer);
                });
            });
        }
        if (!this._config.unauthorized && localStorage.getItem('waw_user')) {
            const user = JSON.parse(localStorage.getItem('waw_user'));
            localStorage.setItem(this._config.name + 'waw_user_id', user._id);
        }
        const url = `${this._url}/get${options.name || ''}`;
        const params = (typeof config.page === 'number' || config.query ? '?' : '') +
            (config.query || '') +
            (typeof config.page === 'number'
                ? `&skip=${this._perPage * (config.page - 1)}&limit=${this._perPage}`
                : '');
        const obs = this.__httpService.get(`${url}${params}`);
        obs.subscribe({
            next: (resp) => {
                resp = resp || [];
                if (typeof config.page !== 'number') {
                    this.clearDocs();
                }
                resp.forEach((doc) => this.addDoc(doc));
                if (options.callback) {
                    options.callback(resp);
                }
                if (typeof config.page !== 'number') {
                    this._filterDocuments();
                    this.__emitterService.complete(this._config.name + '_getted', this._docs);
                }
                this.__emitterService.emit(`${this._config.name}_get`, this._docs);
                this.__emitterService.emit(`${this._config.name}_changed`, this._docs);
            },
            error: (err) => {
                if (options.errCallback) {
                    options.errCallback(err);
                }
            },
        });
        return obs;
    }
    /**
     * Sends a request to the API to create a new document.
     *
     * @param doc - The document to create.
     * @param options - Optional callback and error handling configuration.
     * @returns An observable that resolves with the created document, or emits an error if already created.
     */
    create(doc = {}, options = {}) {
        if (doc._id) {
            return this.update(doc, options);
        }
        doc._localId ||= this._localId();
        doc.__options ||= {};
        doc.__options['create'] = options;
        this.addDoc(doc);
        this._filterDocuments();
        if (!this.__networkService.isOnline()) {
            return new Observable((observer) => {
                this._onOnline.push(() => {
                    this.create(doc, options).subscribe(observer);
                });
            });
        }
        if (doc.__creating) {
            // Emit an error observable if the document is already created
            return new Observable((observer) => {
                observer.error(new Error('Document is currently already creating.'));
            });
        }
        if (this._config.appId) {
            doc.appId = this._config.appId;
        }
        doc.__creating = true;
        const obs = this.__httpService.post(`${this._url}/create${options.name || ''}`, doc);
        obs.subscribe({
            next: (resp) => {
                if (resp) {
                    this.__coreService.copy(resp, doc);
                    this.addDoc(doc);
                    this._filterDocuments();
                    if (options.callback) {
                        options.callback(doc);
                    }
                }
                else {
                    doc.__creating = false;
                    if (options.errCallback) {
                        options.errCallback(resp);
                    }
                }
                this.__emitterService.emit(`${this._config.name}_create`, doc);
                this.__emitterService.emit(`${this._config.name}_list`, doc);
                this.__emitterService.emit(`${this._config.name}_changed`, doc);
            },
            error: (err) => {
                doc.__creating = false;
                if (options.errCallback)
                    options.errCallback(err);
            },
        });
        return obs;
    }
    /**
     * Fetches a document from the API based on a query.
     *
     * @param query - The query object used to filter documents.
     * @param options - Optional callback and error handling configuration.
     * @returns An observable that resolves with the fetched document.
     */
    fetch(query = {}, options = {}) {
        if (!this.__networkService.isOnline()) {
            return new Observable((observer) => {
                this._onOnline.push(() => {
                    this.fetch(query, options).subscribe(observer);
                });
            });
        }
        const obs = this.__httpService.post(`${this._url}/fetch${options.name || ''}`, query);
        obs.subscribe({
            next: (doc) => {
                if (doc) {
                    this.addDoc(doc);
                    this._filterDocuments();
                    if (options.callback)
                        options.callback(doc);
                    this.__emitterService.emit(`${this._config.name}_changed`, doc);
                }
                else {
                    if (options.errCallback) {
                        options.errCallback(doc);
                    }
                }
            },
            error: (err) => {
                if (options.errCallback) {
                    options.errCallback(err);
                }
            },
        });
        return obs;
    }
    /**
     * Updates a document after a specified delay and returns an observable.
     *
     * @param doc - The document to update.
     * @param options - Optional callback and error handling configuration.
     * @returns An observable that emits the updated document.
     */
    updateAfterWhile(doc, options = {}) {
        return new Observable((observer) => {
            this.__coreService.afterWhile(this._id(doc), () => {
                this.update(doc, options).subscribe({
                    next: (updatedDoc) => {
                        observer.next(updatedDoc); // Emit the updated document
                    },
                    error: (err) => {
                        observer.error(err); // Forward the error
                    },
                    complete: () => {
                        observer.complete(); // Complete the observable
                    },
                });
            });
        });
    }
    /**
     * Updates a document in the API.
     *
     * @param doc - The document to update.
     * @param options - Optional callback and error handling configuration.
     * @returns An observable that resolves with the updated document.
     */
    update(doc, options = {}) {
        this._updateModified(doc, 'up' + (options.name || ''), options);
        if (!this.__networkService.isOnline()) {
            return new Observable((observer) => {
                this._onOnline.push(() => {
                    this.update(doc, options).subscribe(observer);
                });
            });
        }
        const obs = this.__httpService.post(`${this._url}/update${options.name || ''}`, doc);
        obs.subscribe({
            next: (resp) => {
                if (resp) {
                    this._removeModified(doc, 'up' + (options.name || ''));
                    const storedDoc = this.doc(doc._id);
                    this.__coreService.copy(resp, storedDoc);
                    this.__coreService.copy(resp, doc);
                    this._syncSignalForDoc(storedDoc);
                    if (options.callback) {
                        options.callback(doc);
                    }
                }
                else {
                    if (options.errCallback) {
                        options.errCallback(resp);
                    }
                }
                this.__emitterService.emit(`${this._config.name}_update`, doc);
                this.__emitterService.emit(`${this._config.name}_changed`, doc);
            },
            error: (err) => {
                if (options.errCallback) {
                    options.errCallback(err);
                }
            },
        });
        return obs;
    }
    /**
     * Unique update a document field in the API.
     *
     * @param doc - The document to update.
     * @param options - Optional callback and error handling configuration.
     * @returns An observable that resolves with the updated document.
     */
    unique(doc, options = {}) {
        this._updateModified(doc, 'un' + (options.name || ''), options);
        if (!this.__networkService.isOnline()) {
            return new Observable((observer) => {
                this._onOnline.push(() => {
                    this.unique(doc, options).subscribe(observer);
                });
            });
        }
        const obs = this.__httpService.post(`${this._url}/unique${options.name || ''}`, doc);
        obs.subscribe({
            next: (resp) => {
                if (resp) {
                    this._removeModified(doc, 'un' + (options.name || ''));
                    doc[options.name] = resp;
                    this._syncSignalForDoc(doc);
                    if (options.callback) {
                        options.callback(doc);
                    }
                }
                else {
                    if (options.errCallback) {
                        options.errCallback(resp);
                    }
                }
                this.__emitterService.emit(`${this._config.name}_unique`, doc);
                this.__emitterService.emit(`${this._config.name}_changed`, doc);
            },
            error: (err) => {
                if (options.errCallback) {
                    options.errCallback(err);
                }
            },
        });
        return obs;
    }
    /**
     * Deletes a document from the API.
     *
     * @param doc - The document to delete.
     * @param options - Optional callback and error handling configuration.
     * @returns An observable that resolves with the deleted document.
     */
    delete(doc, options = {}) {
        doc.__deleted = true;
        doc.__options ||= {};
        doc.__options['delete'] = options;
        this.addDoc(doc);
        this._filterDocuments();
        if (!this.__networkService.isOnline()) {
            return new Observable((observer) => {
                this._onOnline.push(() => {
                    this.delete(doc, options).subscribe(observer);
                });
            });
        }
        const obs = this.__httpService.post(`${this._url}/delete${options.name || ''}`, doc);
        obs.subscribe({
            next: (resp) => {
                if (resp) {
                    const idx = this._docs.findIndex((d) => this._id(d) === this._id(doc));
                    if (idx !== -1) {
                        this._docs.splice(idx, 1);
                    }
                    this.setDocs();
                    // We keep signal but mark it deleted and recompute mappings.
                    this._syncSignalForDoc({
                        ...doc,
                        __deleted: true,
                    });
                    this._filterDocuments();
                    if (options.callback) {
                        options.callback(doc);
                    }
                }
                else {
                    if (options.errCallback) {
                        options.errCallback(resp);
                    }
                }
                this.__emitterService.emit(`${this._config.name}_delete`, doc);
                this.__emitterService.emit(`${this._config.name}_changed`, doc);
            },
            error: (err) => {
                if (options.errCallback) {
                    options.errCallback(err);
                }
            },
        });
        return obs;
    }
    /**
     * Registers a filtered view of documents and returns the recompute callback.
     *
     * The callback is called automatically whenever `_filterDocuments()` runs.
     */
    filteredDocuments(storeObjectOrArray, config = {}) {
        const callback = () => {
            if (Array.isArray(storeObjectOrArray)) {
                let result = this._docs
                    .filter((doc) => !doc.__deleted)
                    .filter(config.valid ?? (() => true));
                storeObjectOrArray.length = 0;
                if (typeof config.sort === 'function') {
                    result = result.sort(config.sort);
                }
                storeObjectOrArray.push(...result);
            }
            else {
                const storeObject = storeObjectOrArray;
                /* remove docs if they were removed */
                for (const parentId in storeObject) {
                    for (let i = storeObject[parentId].length - 1; i >= 0; i--) {
                        const _field = typeof config.field === 'function'
                            ? config.field(storeObject[parentId][i])
                            : config.field || 'author';
                        const _doc = storeObject[parentId][i];
                        if (!this._docs.find((doc) => Array.isArray(doc[_field])
                            ? doc[_field].includes(_doc[this._id(doc)])
                            : doc[_field] === _doc[this._id(doc)])) {
                            storeObject[parentId].splice(i, 1);
                        }
                    }
                }
                /* add docs if they are not added */
                for (const doc of this._docs) {
                    if (doc.__deleted)
                        continue;
                    const _field = typeof config.field === 'function'
                        ? config.field(doc)
                        : config.field || 'author';
                    if (typeof config.valid === 'function'
                        ? !config.valid(doc)
                        : Array.isArray(doc[_field])
                            ? !doc[_field]?.length
                            : !doc[_field]) {
                        continue;
                    }
                    if (typeof config.field === 'function') {
                        if (config.field(doc) &&
                            !storeObject[doc[_field]].find((c) => c._id === doc._id)) {
                            storeObject[doc[_field]].push(doc);
                        }
                    }
                    else if (Array.isArray(doc[_field])) {
                        doc[_field].forEach((_field) => {
                            storeObject[_field] = storeObject[_field] || [];
                            if (!storeObject[_field].find((c) => c._id === doc._id)) {
                                storeObject[_field].push(doc);
                            }
                        });
                    }
                    else {
                        storeObject[doc[_field]] =
                            storeObject[doc[_field]] || [];
                        if (!storeObject[doc[_field]].find((c) => c._id === doc._id)) {
                            storeObject[doc[_field]].push(doc);
                        }
                    }
                }
                /* sort the array's */
                if (typeof config.sort === 'function') {
                    for (const parentId in storeObject) {
                        storeObject[parentId].sort(config.sort);
                    }
                }
            }
            config.filtered?.(storeObjectOrArray);
        };
        this._filteredDocumentsCallbacks.push(callback);
        return callback;
    }
    /**
     * Generates a unique ID for a document when using local-only identifiers.
     *
     * @returns The unique ID as a number.
     */
    _localId() {
        return Number(Date.now() + '' + this._randomCount++);
    }
    /**
     * Returns the configured identity field for the given document as string.
     *
     * @param doc - The document for which to generate the ID.
     * @returns The unique ID as a string.
     */
    _id(doc) {
        return doc[this._config._id || '_id']?.toString();
    }
    /**
     * Executes all registered filter document callbacks and emits a
     * `<name>_filtered` event.
     */
    _filterDocuments() {
        for (const callback of this._filteredDocumentsCallbacks) {
            callback();
        }
        this.__emitterService.emit(`${this._config.name}_filtered`);
    }
    /**
     * Marks a document as modified for a given operation id and
     * keeps the document in the store until the operation is confirmed.
     */
    _updateModified(doc, id, options) {
        doc.__modified ||= [];
        doc.__options ||= {};
        doc.__options[id] = options;
        if (!doc.__modified.find((m) => m === id)) {
            doc.__modified.push(id);
            this.addDoc(doc);
        }
    }
    /**
     * Removes a modification mark from the document once the
     * server operation is confirmed.
     */
    _removeModified(doc, id) {
        doc.__modified ||= [];
        if (doc.__modified.find((m) => m === id)) {
            doc.__modified.splice(doc.__modified.findIndex((m) => m === id), 1);
            this.addDoc(doc);
        }
    }
    /**
     * Syncs a single document's signal (if exists) and refreshes all
     * derived collections (field/value lists and field maps).
     */
    _syncSignalForDoc(doc) {
        const id = this._id(doc);
        if (id && this._signal[id]) {
            this._signal[id].set(doc);
        }
        this._updateSignals();
    }
    /**
     * Rebuilds all derived signal collections:
     *  - all per (field,value) lists of document signals
     *  - all per-field maps value -> [signals]
     *
     * This keeps `getSignals()` and `getFieldSignals()` in sync after
     * any mutation that touches `_docs`.
     */
    _updateSignals() {
        // refresh all (field,value) collections
        for (const key in this._signals) {
            this._signals[key].set(this._getSignals(key));
        }
        // refresh all per-field maps
        for (const field in this._fieldSignals) {
            this._fieldSignals[field].set(this._getFieldSignals(field));
        }
    }
}

/**
 * Utility service for programmatically creating and interacting with Angular
 * components within the DOM.
 */
class DomService {
    constructor() {
        /** Reference to the root application used for view attachment. */
        this._appRef = inject(ApplicationRef);
        /** Injector utilized when creating dynamic components. */
        this._injector = inject(EnvironmentInjector);
        /**
         * Flags to ensure components with a specific `providedIn` key are only
         * instantiated once at a time.
         */
        this._providedIn = {};
    }
    /**
     * Appends a component to a specified element by ID.
     *
     * @param component - The component to append.
     * @param options - The options to project into the component.
     * @param id - The ID of the element to append the component to.
     * @returns An object containing the native element and the component reference.
     */
    appendById(component, options = {}, id) {
        const componentRef = createComponent(component, {
            environmentInjector: this._injector,
        });
        this.projectComponentInputs(componentRef, options);
        this._appRef.attachView(componentRef.hostView);
        const domElem = componentRef.hostView
            .rootNodes[0];
        const element = document.getElementById(id);
        if (element && typeof element.appendChild === 'function') {
            element.appendChild(domElem);
        }
        componentRef.changeDetectorRef.detectChanges();
        return {
            nativeElement: domElem,
            componentRef: componentRef,
            remove: () => this.removeComponent(componentRef),
        };
    }
    /**
     * Appends a component to a specified element or to the body.
     *
     * @param component - The component to append.
     * @param options - The options to project into the component.
     * @param element - The element to append the component to. Defaults to body.
     * @returns An object containing the native element and the component reference.
     */
    appendComponent(component, options = {}, element = document.body) {
        if (options.providedIn) {
            if (this._providedIn[options.providedIn]) {
                return;
            }
            this._providedIn[options.providedIn] = true;
        }
        const componentRef = createComponent(component, {
            environmentInjector: this._injector,
        });
        this.projectComponentInputs(componentRef, options);
        this._appRef.attachView(componentRef.hostView);
        const domElem = componentRef.hostView
            .rootNodes[0];
        if (element && typeof element.appendChild === 'function') {
            element.appendChild(domElem);
        }
        componentRef.changeDetectorRef.detectChanges();
        return {
            nativeElement: domElem,
            componentRef: componentRef,
            remove: () => this.removeComponent(componentRef, options.providedIn),
        };
    }
    /**
     * Gets a reference to a dynamically created component.
     *
     * @param component - The component to create.
     * @param options - The options to project into the component.
     * @returns The component reference.
     */
    getComponentRef(component, options = {}) {
        const componentRef = createComponent(component, {
            environmentInjector: this._injector,
        });
        this.projectComponentInputs(componentRef, options);
        this._appRef.attachView(componentRef.hostView);
        componentRef.changeDetectorRef.detectChanges();
        return componentRef;
    }
    /**
     * Projects the inputs onto the component.
     *
     * @param component - The component reference.
     * @param options - The options to project into the component.
     * @returns The component reference with the projected inputs.
     */
    projectComponentInputs(component, options) {
        if (options) {
            const props = Object.getOwnPropertyNames(options);
            for (const prop of props) {
                component.instance[prop] = options[prop];
            }
        }
        return component;
    }
    /**
     * Removes a previously attached component and optionally clears its
     * unique `providedIn` flag.
     *
     * @param componentRef - Reference to the component to be removed.
     * @param providedIn - Optional key used to track unique instances.
     */
    removeComponent(componentRef, providedIn) {
        this._appRef.detachView(componentRef.hostView);
        componentRef.destroy();
        if (providedIn) {
            delete this._providedIn[providedIn];
        }
    }
    static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: DomService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
    static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: DomService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: DomService, decorators: [{
            type: Injectable,
            args: [{
                    providedIn: 'root',
                }]
        }] });

/**
 * RtcService handles WebRTC peer connections and local media stream setup.
 * It provides functionality to initialize the user's camera/microphone,
 * manage multiple peer connections, and handle offer/answer negotiation.
 */
class RtcService {
    constructor() {
        /**
         * Map of peer connections, keyed by peer ID.
         */
        this.peers = new Map();
        /**
         * Local media stream from user's camera and microphone.
         */
        this.localStream = null;
    }
    /**
     * Initializes the local media stream (audio/video).
     * Requests permissions and stores the stream internally.
     */
    async initLocalStream() {
        if (!this.localStream) {
            this.localStream = await navigator.mediaDevices.getUserMedia({
                video: true,
                audio: true,
            });
        }
        return this.localStream;
    }
    /**
     * Creates a new RTCPeerConnection for the given ID and attaches local tracks.
     */
    async createPeer(id) {
        const peer = new RTCPeerConnection();
        this.localStream
            ?.getTracks()
            .forEach((track) => peer.addTrack(track, this.localStream));
        this.peers.set(id, peer);
        return peer;
    }
    /**
     * Retrieves an existing peer connection by ID.
     */
    getPeer(id) {
        return this.peers.get(id);
    }
    /**
     * Creates an SDP offer for the specified peer and sets it as the local description.
     */
    async createOffer(id) {
        const peer = this.peers.get(id);
        if (!peer)
            throw new Error('Peer not found');
        const offer = await peer.createOffer();
        await peer.setLocalDescription(offer);
        return offer;
    }
    /**
     * Accepts an SDP offer, creates an answer, and sets it as the local description.
     */
    async createAnswer(id, offer) {
        const peer = this.peers.get(id);
        if (!peer)
            throw new Error('Peer not found');
        await peer.setRemoteDescription(new RTCSessionDescription(offer));
        const answer = await peer.createAnswer();
        await peer.setLocalDescription(answer);
        return answer;
    }
    /**
     * Sets the remote description with an SDP answer for the given peer.
     */
    async setRemoteAnswer(id, answer) {
        const peer = this.peers.get(id);
        if (!peer)
            throw new Error('Peer not found');
        await peer.setRemoteDescription(new RTCSessionDescription(answer));
    }
    /**
     * Adds an ICE candidate to the specified peer connection.
     */
    addIceCandidate(id, candidate) {
        const peer = this.peers.get(id);
        if (peer)
            peer.addIceCandidate(new RTCIceCandidate(candidate));
    }
    /**
     * Returns the initialized local media stream.
     */
    getLocalStream() {
        return this.localStream;
    }
    /**
     * Closes a specific peer connection and removes it from the map.
     */
    closePeer(id) {
        const peer = this.peers.get(id);
        if (peer) {
            peer.close();
            this.peers.delete(id);
        }
    }
    /**
     * Closes all peer connections and stops the local media stream.
     */
    closeAll() {
        this.peers.forEach((peer) => peer.close());
        this.peers.clear();
        this.localStream?.getTracks().forEach((track) => track.stop());
        this.localStream = null;
    }
    static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: RtcService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
    static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: RtcService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: RtcService, decorators: [{
            type: Injectable,
            args: [{ providedIn: 'root' }]
        }] });

class SocketService {
    constructor(_config) {
        this._config = _config;
        this._url = '';
        this._connected = false;
        this._opts = {};
        this._emitterService = inject(EmitterService);
        this._config = { ...DEFAULT_CONFIG, ...(this._config || {}) };
        if (!this._config.io) {
            return;
        }
        const url = new URL(window.location.origin);
        if (typeof this._config.socket === 'object') {
            if (this._config.socket.port) {
                url.port = this._config.socket.port;
            }
            if (this._config.socket.opts) {
                this._opts = this._config.socket.opts;
            }
            this._url = this._config.socket.url ?? url.origin;
        }
        else {
            this._url = url.origin;
        }
        if (this._config.socket) {
            this.load();
        }
    }
    /**
     * Sets the URL for the WebSocket connection and reloads the socket.
     * @param url - The URL of the WebSocket server.
     */
    setUrl(url) {
        this._url = url;
        if (!this._config.socket) {
            this._config.socket = true;
        }
        this.load();
    }
    /**
     * Loads and initializes the WebSocket connection.
     */
    load() {
        if (this._config.io) {
            const ioFunc = this._config.io.default
                ? this._config.io.default
                : this._config.io;
            this._io = ioFunc(this._url, this._opts);
            this._io.on('connect', () => {
                this._connected = true;
                this._emitterService.complete('socket');
            });
            this._io.on('disconnect', (reason) => {
                this._connected = false;
                this._emitterService.emit('socket_disconnect', reason);
                console.warn('Socket disconnected', reason);
            });
            this._io.on('error', (err) => {
                this._connected = false;
                this._emitterService.emit('socket_error', err);
                console.warn('Socket error', err);
            });
        }
    }
    /**
     * Disconnects the WebSocket connection and resets the connection state.
     */
    disconnect() {
        if (this._io) {
            this._io.disconnect();
        }
        this._connected = false;
    }
    /**
     * Subscribes to a WebSocket event.
     * @param to - The event to subscribe to.
     * @param cb - The callback function to execute when the event is received.
     */
    on(to, cb = () => { }) {
        if (!this._config.socket) {
            return;
        }
        if (!this._io) {
            console.warn('Socket client not loaded.');
            return;
        }
        if (!this._connected) {
            setTimeout(() => {
                this.on(to, cb);
            }, 100);
            return;
        }
        this._io.on(to, cb);
    }
    /**
     * Emits a message to a WebSocket event.
     * @param to - The event to emit the message to.
     * @param message - The message to emit.
     * @param room - Optional room to emit the message to.
     */
    emit(to, message, room = false) {
        if (!this._config.socket) {
            return;
        }
        if (!this._io) {
            console.warn('Socket client not loaded.');
            return;
        }
        if (!this._connected) {
            setTimeout(() => {
                this.emit(to, message, room);
            }, 100);
            return;
        }
        this._io.emit(to, message, room);
    }
    static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: SocketService, deps: [{ token: CONFIG_TOKEN, optional: true }], target: i0.ɵɵFactoryTarget.Injectable }); }
    static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: SocketService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: SocketService, decorators: [{
            type: Injectable,
            args: [{
                    providedIn: 'root',
                }]
        }], ctorParameters: () => [{ type: undefined, decorators: [{
                    type: Inject,
                    args: [CONFIG_TOKEN]
                }, {
                    type: Optional
                }] }] });

class TimeService {
    constructor(datePipe) {
        this.datePipe = datePipe;
        this.weekDays = [
            'Sunday',
            'Monday',
            'Tuesday',
            'Wednesday',
            'Thursday',
            'Friday',
            'Saturday',
        ];
        this.monthNames = [
            'January',
            'February',
            'March',
            'April',
            'May',
            'June',
            'July',
            'August',
            'September',
            'October',
            'November',
            'December',
        ];
    }
    /**
     * Returns the name of the day of the week for a given date.
     *
     * @param date - The date for which to get the day of the week.
     * @param format - The format in which to return the day name. Default is 'long'.
     * @returns The name of the day of the week.
     */
    getDayName(date, format = 'long') {
        const dayIndex = date.getDay();
        return format === 'short'
            ? this.weekDays[dayIndex].substring(0, 3)
            : this.weekDays[dayIndex];
    }
    /**
     * Returns the name of the month for a given index.
     *
     * @param monthIndex - The month index (0-11).
     * @param format - The format in which to return the month name. Default is 'long'.
     * @returns The name of the month.
     */
    getMonthName(monthIndex, format = 'long') {
        if (!Number.isInteger(monthIndex) ||
            monthIndex < 0 ||
            monthIndex > 11) {
            throw new RangeError('monthIndex must be an integer between 0 and 11');
        }
        return format === 'short'
            ? this.monthNames[monthIndex].substring(0, 3)
            : this.monthNames[monthIndex];
    }
    /**
     * Formats a date according to the specified format and timezone.
     *
     * @param date - The date to format.
     * @param format - The format string (see Angular DatePipe documentation for format options).
     * @param timezone - The timezone to use for formatting.
     * @returns The formatted date string.
     */
    formatDate(date, format = 'mediumDate', timezone = 'UTC') {
        return this.datePipe.transform(date, format, timezone) || '';
    }
    /**
     * Converts a date to a different timezone.
     *
     * @param date - The date to convert.
     * @param timezone - The timezone to convert to.
     * @returns The date in the new timezone.
     */
    convertToTimezone(date, timezone) {
        return new Date(date.toLocaleString('en-US', { timeZone: timezone }));
    }
    /**
     * Returns the start of the day for a given date.
     *
     * @param date - The date for which to get the start of the day.
     * @returns The start of the day (midnight) for the given date.
     */
    startOfDay(date) {
        const newDate = new Date(date);
        newDate.setHours(0, 0, 0, 0);
        return newDate;
    }
    /**
     * Returns the end of the day for a given date.
     *
     * @param date - The date for which to get the end of the day.
     * @returns The end of the day (one millisecond before midnight) for the given date.
     */
    endOfDay(date) {
        const newDate = new Date(date);
        newDate.setHours(23, 59, 59, 999);
        return newDate;
    }
    /**
     * Returns the start of the week for a given date.
     *
     * @param date - The date for which to get the start of the week.
     * @param locale - A BCP 47 language tag to determine the first day of the week. Defaults to the runtime locale.
     * @returns The start of the week adjusted for the locale.
     *
     * @example
     * const date = new Date('2024-05-15');
     * service.startOfWeek(date); // => Monday May 13 2024 00:00:00 for en-GB
     */
    startOfWeek(date, locale) {
        const newDate = this.startOfDay(date);
        const dtf = new Intl.DateTimeFormat(locale);
        const resolved = dtf.resolvedOptions().locale;
        const region = resolved.split('-')[1]?.toUpperCase();
        const sundayFirst = ['US', 'CA', 'AU', 'NZ', 'PH', 'BR'];
        const firstDay = sundayFirst.includes(region) ? 0 : 1;
        const day = newDate.getDay();
        const diff = (day - firstDay + 7) % 7;
        newDate.setDate(newDate.getDate() - diff);
        return newDate;
    }
    /**
     * Returns the end of the week for a given date.
     *
     * @param date - The date for which to get the end of the week.
     * @param locale - A BCP 47 language tag to determine the first day of the week. Defaults to the runtime locale.
     * @returns The end of the week adjusted for the locale.
     *
     * @example
     * const date = new Date('2024-05-15');
     * service.endOfWeek(date); // => Sunday May 19 2024 23:59:59.999 for en-GB
     */
    endOfWeek(date, locale) {
        const start = this.startOfWeek(date, locale);
        const end = this.addDays(start, 6);
        return this.endOfDay(end);
    }
    /**
     * Returns the start of the month for a given date.
     *
     * @param date - The date for which to get the start of the month.
     * @returns The start of the month.
     *
     * @example
     * const date = new Date('2024-05-15');
     * service.startOfMonth(date); // => May 1 2024 00:00:00
     */
    startOfMonth(date) {
        const newDate = this.startOfDay(date);
        newDate.setDate(1);
        return newDate;
    }
    /**
     * Returns the end of the month for a given date.
     *
     * @param date - The date for which to get the end of the month.
     * @returns The end of the month.
     *
     * @example
     * const date = new Date('2024-05-15');
     * service.endOfMonth(date); // => May 31 2024 23:59:59.999
     */
    endOfMonth(date) {
        const start = this.startOfMonth(date);
        const end = new Date(start);
        end.setMonth(end.getMonth() + 1);
        end.setDate(0);
        return this.endOfDay(end);
    }
    /**
     * Returns the start of the year for a given date.
     *
     * @param date - The date for which to get the start of the year.
     * @returns The start of the year.
     *
     * @example
     * const date = new Date('2024-05-15');
     * service.startOfYear(date); // => Jan 1 2024 00:00:00
     */
    startOfYear(date) {
        const newDate = this.startOfDay(date);
        newDate.setMonth(0, 1);
        return newDate;
    }
    /**
     * Returns the end of the year for a given date.
     *
     * @param date - The date for which to get the end of the year.
     * @returns The end of the year.
     *
     * @example
     * const date = new Date('2024-05-15');
     * service.endOfYear(date); // => Dec 31 2024 23:59:59.999
     */
    endOfYear(date) {
        const end = new Date(date.getFullYear(), 11, 31);
        return this.endOfDay(end);
    }
    /**
     * Returns the number of days in a given month and year.
     *
     * @param month - The month (0-11).
     * @param year - The year.
     * @returns The number of days in the month.
     */
    getDaysInMonth(month, year) {
        return new Date(year, month + 1, 0).getDate();
    }
    /**
     * Checks if a given year is a leap year.
     *
     * @param year - The year to check.
     * @returns True if the year is a leap year, false otherwise.
     */
    isLeapYear(year) {
        return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
    }
    /**
     * Adds a specified number of days to a date.
     *
     * @param date - The date to which to add days.
     * @param days - The number of days to add.
     * @returns The new date with the added days.
     */
    addDays(date, days) {
        const newDate = new Date(date);
        newDate.setDate(newDate.getDate() + days);
        return newDate;
    }
    /**
     * Adds a specified number of months to a date.
     *
     * @param date - The date to which to add months.
     * @param months - The number of months to add.
     * @returns The new date with the added months.
     */
    addMonths(date, months) {
        const newDate = new Date(date);
        newDate.setMonth(newDate.getMonth() + months);
        return newDate;
    }
    /**
     * Adds a specified number of years to a date.
     *
     * @param date - The date to which to add years.
     * @param years - The number of years to add.
     * @returns The new date with the added years.
     */
    addYears(date, years) {
        const newDate = new Date(date);
        newDate.setFullYear(newDate.getFullYear() + years);
        return newDate;
    }
    /**
     * Adds a specified number of hours to a date.
     *
     * @param date - The date to which to add hours.
     * @param hours - The number of hours to add.
     * @returns The new date with the added hours.
     */
    addHours(date, hours) {
        const newDate = new Date(date);
        newDate.setHours(newDate.getHours() + hours);
        return newDate;
    }
    /**
     * Adds a specified number of minutes to a date.
     *
     * @param date - The date to which to add minutes.
     * @param minutes - The number of minutes to add.
     * @returns The new date with the added minutes.
     */
    addMinutes(date, minutes) {
        const newDate = new Date(date);
        newDate.setMinutes(newDate.getMinutes() + minutes);
        return newDate;
    }
    /**
     * Adds a specified number of seconds to a date.
     *
     * @param date - The date to which to add seconds.
     * @param seconds - The number of seconds to add.
     * @returns The new date with the added seconds.
     */
    addSeconds(date, seconds) {
        const newDate = new Date(date);
        newDate.setSeconds(newDate.getSeconds() + seconds);
        return newDate;
    }
    /**
     * Subtracts a specified number of days from a date.
     *
     * @param date - The date from which to subtract days.
     * @param days - The number of days to subtract.
     * @returns The new date with the subtracted days.
     */
    subtractDays(date, days) {
        return this.addDays(date, -days);
    }
    /**
     * Subtracts a specified number of months from a date.
     *
     * @param date - The date from which to subtract months.
     * @param months - The number of months to subtract.
     * @returns The new date with the subtracted months.
     */
    subtractMonths(date, months) {
        return this.addMonths(date, -months);
    }
    /**
     * Subtracts a specified number of years from a date.
     *
     * @param date - The date from which to subtract years.
     * @param years - The number of years to subtract.
     * @returns The new date with the subtracted years.
     */
    subtractYears(date, years) {
        return this.addYears(date, -years);
    }
    /**
     * Subtracts a specified number of hours from a date.
     *
     * @param date - The date from which to subtract hours.
     * @param hours - The number of hours to subtract.
     * @returns The new date with the subtracted hours.
     */
    subtractHours(date, hours) {
        return this.addHours(date, -hours);
    }
    /**
     * Subtracts a specified number of minutes from a date.
     *
     * @param date - The date from which to subtract minutes.
     * @param minutes - The number of minutes to subtract.
     * @returns The new date with the subtracted minutes.
     */
    subtractMinutes(date, minutes) {
        return this.addMinutes(date, -minutes);
    }
    /**
     * Subtracts a specified number of seconds from a date.
     *
     * @param date - The date from which to subtract seconds.
     * @param seconds - The number of seconds to subtract.
     * @returns The new date with the subtracted seconds.
     */
    subtractSeconds(date, seconds) {
        return this.addSeconds(date, -seconds);
    }
    /**
     * Calculates the difference in days between two dates.
     *
     * @param date1 - The earlier date.
     * @param date2 - The later date.
     * @returns The number of days between the two dates.
     */
    differenceInDays(date1, date2) {
        const diff = date2.getTime() - date1.getTime();
        return diff / (1000 * 60 * 60 * 24);
    }
    /**
     * Calculates the difference in hours between two dates.
     *
     * @param date1 - The earlier date.
     * @param date2 - The later date.
     * @returns The number of hours between the two dates.
     */
    differenceInHours(date1, date2) {
        const diff = date2.getTime() - date1.getTime();
        return diff / (1000 * 60 * 60);
    }
    /**
     * Calculates the difference in minutes between two dates.
     *
     * @param date1 - The earlier date.
     * @param date2 - The later date.
     * @returns The number of minutes between the two dates.
     */
    differenceInMinutes(date1, date2) {
        const diff = date2.getTime() - date1.getTime();
        return diff / (1000 * 60);
    }
    /**
     * Checks if two dates are on the same day.
     *
     * @param date1 - The first date.
     * @param date2 - The second date.
     * @returns True if the dates are on the same day, false otherwise.
     */
    isSameDay(date1, date2) {
        return (date1.getFullYear() === date2.getFullYear() &&
            date1.getMonth() === date2.getMonth() &&
            date1.getDate() === date2.getDate());
    }
    /**
     * Returns the ISO week number for a given date.
     *
     * @param date - The date for which to get the week number.
     * @returns The ISO week number (1-53).
     */
    getWeekNumber(date) {
        const tempDate = new Date(date.getTime());
        tempDate.setHours(0, 0, 0, 0);
        // Set to nearest Thursday: current date + 4 - current day number, making Thursday day 4
        tempDate.setDate(tempDate.getDate() + 4 - (tempDate.getDay() || 7));
        const yearStart = new Date(tempDate.getFullYear(), 0, 1);
        // Calculate full weeks to nearest Thursday
        return Math.ceil(((tempDate.getTime() - yearStart.getTime()) / 86400000 + 1) / 7);
    }
    /**
     * Returns the number of weeks in a month for a given month and year.
     *
     * @param month - The month (0-11).
     * @param year - The year.
     * @returns The number of weeks in the month.
     */
    getWeeksInMonth(month, year) {
        const firstDayOfMonth = new Date(year, month, 1);
        const lastDayOfMonth = new Date(year, month + 1, 0);
        // Get ISO week numbers for the first and last day of the month
        const firstWeek = this.getWeekNumber(firstDayOfMonth);
        let lastWeek = this.getWeekNumber(lastDayOfMonth);
        // Special case: when January 1st is in the last week of the previous year
        if (firstWeek > lastWeek) {
            lastWeek = this.getWeekNumber(new Date(year, 11, 31)); // Get week of the last day of the year
        }
        return lastWeek - firstWeek + 1;
    }
    static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TimeService, deps: [{ token: i1$2.DatePipe }], target: i0.ɵɵFactoryTarget.Injectable }); }
    static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TimeService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TimeService, decorators: [{
            type: Injectable,
            args: [{
                    providedIn: 'root',
                }]
        }], ctorParameters: () => [{ type: i1$2.DatePipe }] });

class UtilService {
    /**
     * Initialize service state: load persisted CSS variables from localStorage
     * and apply them to the document root. Emits the initial CSS snapshot.
     */
    constructor() {
        // --- CSS variables (persisted) ---
        this._storageKey = 'css_variables';
        this._css = {};
        this._cssSig = signal({}, ...(ngDevMode ? [{ debugName: "_cssSig" }] : []));
        // --- Forms store ---
        this._forms = new Map();
        // --- Global bag for design/debug ---
        this.var = {};
        this._loadCss();
        // apply on boot
        for (const k of Object.keys(this._css))
            this._setProperty(k, this._css[k]);
        this._cssSig.set({ ...this._css });
    }
    // ===== Forms Management =====
    /** Get or create a form state as a writable signal */
    formSignal(id) {
        let s = this._forms.get(id);
        if (!s) {
            s = signal({});
            this._forms.set(id, s);
        }
        return s;
    }
    /** Back-compat: returns the current form object (mutable reference). Prefer formSignal(). */
    form(id) {
        const s = this.formSignal(id);
        const v = s();
        if (v && typeof v === 'object')
            return v;
        const obj = {};
        s.set(obj);
        return obj;
    }
    /**
     * Check whether a form signal has been created for the given id.
     * @param id Unique form identifier.
     * @returns True if a signal exists for the id.
     */
    hasForm(id) {
        return this._forms.has(id);
    }
    /**
     * Remove form state associated with the given id.
     * @param id Unique form identifier to clear.
     */
    clearForm(id) {
        this._forms.delete(id);
    }
    // ===== Validation =====
    /**
     * Validate a value against a specific kind.
     * - email: RFC-light pattern check.
     * - text: typeof string.
     * - array: Array.isArray.
     * - object: non-null plain object.
     * - number: finite number.
     * - password: strength tiers (extra 0..4).
     * @param value Input to validate.
     * @param kind Validation kind.
     * @param extra Additional constraint for passwords: 0..4 increasing strength.
     * @returns True if value passes validation.
     */
    valid(value, kind = 'email', extra = 0) {
        switch (kind) {
            case 'email':
                return /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,10})+$/.test(value || '');
            case 'text':
                return typeof value === 'string';
            case 'array':
                return Array.isArray(value);
            case 'object':
                return (typeof value === 'object' &&
                    !Array.isArray(value) &&
                    value !== null);
            case 'number':
                return typeof value === 'number' && Number.isFinite(value);
            case 'password':
                if (!value)
                    return false;
                switch (extra) {
                    case 1:
                        return /^((?=.*[a-z])(?=.*[0-9]))|((?=.*[A-Z])(?=.*[0-9]))/.test(value);
                    case 2:
                        return /^(((?=.*[a-z])(?=.*[0-9]))|((?=.*[A-Z])(?=.*[0-9])))(?=.{8,})/.test(value);
                    case 3:
                        return /^((?=.*[a-z])(?=.*[A-Z])(?=.*[0-9]))(?=.{8,})/.test(value);
                    case 4:
                        return /^((?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[@#$%&!\-_]))(?=.{8,})/.test(value);
                    default:
                        return !!value;
                }
        }
    }
    /** Password strength: 0..5 */
    level(value = '') {
        if (!value)
            return 0;
        let lvl = 0;
        if (value.length > 8)
            lvl++;
        if (/[a-z]/.test(value))
            lvl++;
        if (/[A-Z]/.test(value))
            lvl++;
        if (/[0-9]/.test(value))
            lvl++;
        if (/[`~!@#$%^&*()_\-+=\[\]{};:'",.<>/?\\|]/.test(value))
            lvl++;
        return lvl;
    }
    // ===== CSS Variables Management =====
    /** Set multiple CSS vars. opts: { local?: boolean; host?: string } */
    setCss(vars, opts = {}) {
        if (typeof opts === 'string') {
            opts = opts === 'local' ? { local: true } : { host: opts };
        }
        const { local = false, host } = opts;
        if (host &&
            typeof window !== 'undefined' &&
            window.location.host !== host)
            return;
        for (const k of Object.keys(vars)) {
            const v = vars[k];
            if (local) {
                this._css[k] = v;
            }
            else if (this._css[k]) {
                // keep persisted value unless explicitly local
                this._setProperty(k, this._css[k]);
                continue;
            }
            this._setProperty(k, v);
        }
        if (local) {
            this._saveCss();
            this._cssSig.set({ ...this._css });
        }
    }
    /** Current persisted CSS vars snapshot */
    getCss() {
        return { ...this._css };
    }
    /** Reactive signal with current persisted CSS vars */
    cssSignal() {
        return this._cssSig;
    }
    /** Remove persisted vars by key(s) and save */
    removeCss(keys) {
        const list = Array.isArray(keys) ? keys : keys.split(' ');
        for (const k of list)
            delete this._css[k];
        this._saveCss();
        this._cssSig.set({ ...this._css });
    }
    // ===== Generators =====
    /**
     * Generate an array of given length filled with sequential numbers (1..n),
     * random text, dates from today, or a constant value.
     * @param len Desired array length.
     * @param type 'number' | 'text' | 'date' | any other constant value to repeat.
     * @returns Generated array.
     */
    arr(len = 10, type = 'number') {
        const out = [];
        for (let i = 0; i < len; i++) {
            switch (type) {
                case 'number':
                    out.push(i + 1);
                    break;
                case 'text':
                    out.push(this.text());
                    break;
                case 'date':
                    out.push(new Date(Date.now() + i * 86_400_000));
                    break;
                default:
                    out.push(type);
            }
        }
        return out;
    }
    /**
     * Create a random alphanumeric string.
     * @param length Number of characters to generate.
     * @returns Random string of requested length.
     */
    text(length = 10) {
        const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
        let res = '';
        for (let i = 0; i < length; i++)
            res += chars.charAt(Math.floor(Math.random() * chars.length));
        return res;
    }
    // ===== Internals =====
    /** Persist current CSS variables to localStorage. */
    _saveCss() {
        try {
            if (typeof localStorage !== 'undefined') {
                localStorage.setItem(this._storageKey, JSON.stringify(this._css));
            }
        }
        catch { }
    }
    /** Load CSS variables snapshot from localStorage into memory. */
    _loadCss() {
        try {
            if (typeof localStorage !== 'undefined') {
                const raw = localStorage.getItem(this._storageKey);
                this._css = raw ? JSON.parse(raw) : {};
            }
        }
        catch {
            this._css = {};
        }
    }
    /**
     * Apply a CSS variable to the :root element.
     * @param key CSS custom property name (e.g., --brand-color).
     * @param value CSS value to set.
     */
    _setProperty(key, value) {
        try {
            if (typeof document !== 'undefined') {
                document.documentElement.style.setProperty(key, value);
            }
        }
        catch { }
    }
    static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: UtilService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
    static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: UtilService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: UtilService, decorators: [{
            type: Injectable,
            args: [{ providedIn: 'root' }]
        }], ctorParameters: () => [] });

function provideWacom(config = DEFAULT_CONFIG) {
    return makeEnvironmentProviders([
        { provide: CONFIG_TOKEN, useValue: config },
        provideHttpClient(withInterceptorsFromDi()),
    ]);
}

/* initialize */
const DIRECTIVES = [ClickOutsideDirective];
const PIPES = [
    ArrPipe,
    SafePipe,
    SplicePipe,
    SearchPipe,
    MongodatePipe,
    PaginationPipe,
];
/**
 * @deprecated Use provideWacom instead.
 */
class WacomModule {
    static forRoot(config = DEFAULT_CONFIG) {
        return {
            ngModule: WacomModule,
            providers: [
                {
                    provide: CONFIG_TOKEN,
                    useValue: config,
                },
            ],
        };
    }
    static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: WacomModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
    static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "21.1.0", ngImport: i0, type: WacomModule, imports: [CommonModule, FormsModule, ArrPipe,
            SafePipe,
            SplicePipe,
            SearchPipe,
            MongodatePipe,
            PaginationPipe, ClickOutsideDirective], exports: [ArrPipe,
            SafePipe,
            SplicePipe,
            SearchPipe,
            MongodatePipe,
            PaginationPipe, ClickOutsideDirective] }); }
    static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: WacomModule, providers: [
            { provide: CONFIG_TOKEN, useValue: DEFAULT_CONFIG },
            provideHttpClient(withInterceptorsFromDi()),
        ], imports: [CommonModule, FormsModule] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: WacomModule, decorators: [{
            type: NgModule,
            args: [{
                    imports: [CommonModule, FormsModule, ...PIPES, ...DIRECTIVES],
                    exports: [...PIPES, ...DIRECTIVES],
                    providers: [
                        { provide: CONFIG_TOKEN, useValue: DEFAULT_CONFIG },
                        provideHttpClient(withInterceptorsFromDi()),
                    ],
                }]
        }] });

/*
 *	Interfaces
 */
/*
 *	End of Support
 */

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

export { ArrPipe, CONFIG_TOKEN, ClickOutsideDirective, CoreService, CrudComponent, CrudService, DEFAULT_CONFIG, DEFAULT_HTTP_CONFIG, DEFAULT_NETWORK_CONFIG, DomService, EmitterService, HttpService, ManualDisabledDirective, ManualNameDirective, ManualReadonlyDirective, ManualTypeDirective, MetaGuard, MetaService, MongodatePipe, NETWORK_CONFIG, NetworkService, NumberPipe, PaginationPipe, RtcService, SafePipe, SearchPipe, SocketService, SplicePipe, SplitPipe, StoreService, TimeService, UtilService, WacomModule, provideWacom };
//# sourceMappingURL=wacom.mjs.map