import * as _angular_core from '@angular/core'; import { Type, Signal, OnInit, OnDestroy, ElementRef, Renderer2, Injector, EnvironmentInjector, AfterViewInit, EventEmitter } from '@angular/core'; import * as eru_studio from 'eru-studio'; import { Observable } from 'rxjs'; import { FormControl, ValidatorFn, FormGroup, AbstractControl } from '@angular/forms'; import { MatTreeNestedDataSource } from '@angular/material/tree'; import { NestedTreeControl } from '@angular/cdk/tree'; import { EruGridComponent, GridConfiguration, GridAction, GridPreset, Field } from 'eru-grid'; import * as echarts from 'echarts/core'; import { MatChipInputEvent } from '@angular/material/chips'; interface ComponentStyles { classes: string; responsive_classes: ResponsiveClasses; responsive_styles: ResponsiveStyleProperties; custom: Record; } interface ComponentPosition { x: number; y: number; width?: number; height?: number; z_index?: number; } interface ComponentDefinition { type: string; name: string; category: ComponentCategory; icon: string; component?: Type; allow_children: boolean; allowed_parents?: string[]; events?: { label: string; value: string; }[]; } interface PropertySchema { key: string; label: string; type: PropertyType; category: PropertyCategory; options?: SelectOption[]; option_type?: 'static' | 'entity' | 'api'; dynamic_options?: string; validation?: ValidationRule[]; responsive?: boolean; visible?: boolean; section?: string; default_value?: any; description?: string; width?: 1 | 2 | 'half' | 'full'; min?: number; max?: number; step?: number; orientation?: 'horizontal' | 'vertical'; visible_if?: VisibilityRule | VisibilityRule[]; visible_operator?: 'all' | 'any' | 'none'; disable_if?: DisableRule | DisableRule[]; disable_operator?: 'all' | 'any' | 'none'; on_value_change?: (value: any, oldValue: any) => void; } interface ColorRange { from?: number | null; to?: number | null; color?: string; background?: string; } interface SelectOption { label: string; value: any; /** * Set only for the ENTITY_DATA option source, where `value` is the record's * `entity_id` and `label` is its display text. Two records can share the * text, so the id is what identifies a selection — see the `_` proxy * key in the select component. */ entity_id?: any; entity_data?: any; preview?: string; preview_color?: string; } interface ValidationRule { type: 'required' | 'min' | 'max' | 'minLength' | 'maxLength' | 'pattern' | 'email' | 'custom'; value?: any; message: string; } interface VisibilityRule { key: string; equals?: any; notEquals?: any; in?: any[]; notIn?: any[]; } interface ComponentCondition { field: string; operator: 'equals' | 'notEquals' | 'in' | 'notIn' | 'contains' | 'greaterThan' | 'lessThan' | 'empty' | 'notEmpty'; value?: any; } interface DisableRule { key?: string; equals?: any; notEquals?: any; in?: any[]; notIn?: any[]; evaluate?: (getPropertyValue: (key: string) => any) => boolean; } type ComponentCategory = 'basic' | 'layout' | 'form' | 'data' | 'navigation' | 'media' | 'eru' | 'loading'; type PropertyType = 'text' | 'date' | 'textarea' | 'number' | 'select' | 'color' | 'boolean' | 'spacing' | 'border' | 'image' | 'icon' | 'radio' | 'multiselect' | 'payload_fields' | 'autocomplete' | 'logic_editor' | 'status_options' | 'tabs_list' | 'menu_items' | 'toolbar_items' | 'color_ranges' | 'dependent_fields' | 'pivot_aggregations' | 'chart_dimensions' | 'chart_measures' | 'chart_extra_options' | 'chart_value_colors'; type PropertyCategory = 'data' | 'content' | 'layout' | 'behavior' | 'events'; interface ComponentEvent { id: string; properties: EruComponentProperties; event: ComponentEventSubscription; timestamp: number; data?: any; page_data?: PageData | null; } interface EventSubscription { componentId: string; componentName: string; events: string[]; } interface EventSchema { key: string; label: string; type: 'multiselect'; category: 'events'; options: SelectOption[]; default_value: string[]; description?: string; width?: 1 | 2 | 'half' | 'full'; } /** * A message from a field's server-side validation check, queued for the host app * to display. The libraries do not render a snackbar themselves — the host does, * red when `status` is 'error' — and removes the notice once shown. */ interface ServerValidationNotice { id?: string; timestamp?: number; componentId: string; field_name: string; status: 'success' | 'error'; msg: string; } interface PageStateVariable { key: string; initial: any; formula?: StateFormula; } interface StateFilter { field: string; equals?: any; not_empty?: boolean; in?: any[]; not_in?: any[]; } interface StateFormula { fn: 'count' | 'sum' | 'avg' | 'min' | 'max' | 'expr'; /** * Rows the aggregate runs over. * * 'pageDataArray' (the default) is the page's own record array. `'state:'` * aggregates over an array held in page state instead — which is how a grid's * selected rows get counted or summed: the grid's On Selection Change writes * `selection.selected_rows` into state, and a formula reads it back from there * — which needs the grid's Send Selected Rows in Event turned on, since that * array is empty by default. */ source?: 'pageDataArray' | string; field?: string; filter?: StateFilter | StateFilter[]; value?: string; } interface UpdateStateFormula { fn: 'set' | 'increment' | 'decrement' | 'toggle' | 'set-from-field' | 'set-from-payload' | 'reset' | 'expr'; value?: any; by?: number; values?: any[]; field?: string; expr?: string; payload_path?: string; } interface EruPage { id: string; name: string; title?: string; entity_name?: string; route?: string; components: EruComponent[]; styles: PageStyles; state?: PageStateVariable[]; display_mode?: 'inline' | 'popup' | 'side_panel'; parent_page_id?: string; master_detail_config?: MasterDetailConfig; events?: ComponentEventSubscription[]; /** * Seed this page's data from a state variable when it is opened on its own * (navigate-to-page, deep link, viewer) — the standalone equivalent of the * page_ref `data_source: 'state'` mapping. * * Inside a page_ref the mount point wins: a page_ref with its own * `data_source: 'state'` feeds the page, so the same page can be reused by * several page_refs each handing it a different record. When the page_ref * declares no state source, this declaration applies there too. * * 'none' (or absent) leaves page data alone. */ data_source?: 'none' | 'state'; /** Which store the field is read from. Defaults to 'page'. */ state_scope?: 'page' | 'app'; /** State variable holding the record for this page. */ state_field?: string; /** Optional dotted path into that value before it is used. */ state_result_path?: string; } interface PageStyles { classes: string; responsive_classes: ResponsiveClasses; responsive_styles: ResponsiveStyleProperties; custom: Record; } type TailwindBreakpoint = 'base' | 'sm' | 'md' | 'lg' | 'xl' | '2xl'; /** * One query param written alongside the page id by a `navigate-to-page` * action. The `param` name should match a state variable declared on the * target page — `initPageState` seeds that variable from the URL on mount. * * Values are scalars by design (ids, flags, tab names): the URL is the * transport, so the value survives refresh, Back and link sharing. Set * `encode: 'json'` to pass a small structured value; larger payloads belong * in app state (`state_scope: 'app'`), with the URL carrying only a handle. */ interface NavParam { param: string; value?: any; value_expression?: string; encode?: 'json'; } /** Marks a query param whose value is JSON rather than a plain scalar. */ declare const NAV_PARAM_JSON_PREFIX = "json:"; /** Longest nav param value written to the URL; beyond this it is dropped with a warning. */ declare const NAV_PARAM_MAX_LENGTH = 1500; interface ComponentEventSubscription { id: string; event: string; action: 'no-action' | 'call-function' | 'call-query' | 'fetch-page-data' | 'hide-fields' | 'unhide-fields' | 'save-page-data' | 'start-loading' | 'stop-loading' | 'hide-component' | 'show-component' | 'disable-field' | 'enable-field' | 'update-state' | 'start-timer' | 'stop-timer' | 'set-field' | 'update-property' | 'enable-component' | 'disable-component' | 'refresh-grid' | 'download-grid' | 'refresh-page-ref' | 'step-forward' | 'step-back' | 'set-active-tab' | 'emit-to-parent' | 'toggle-side-panel' | 'open-side-panel' | 'close-side-panel' | 'navigate-to-page' | 'set-page-data' | 'clear-page-data' | 'clear-all-page-data' | 'download-file'; apiName?: string; function_name?: string; query_name?: string; api_payload_fields?: string[]; fieldNames?: string[]; page_id?: string; payload?: any; state_key?: string; state_formula?: UpdateStateFormula; record_source?: 'event' | 'state' | 'app_state'; record_path?: string; file_source?: 'response' | 'state' | 'app_state' | 'page_data'; file_path?: string; file_type?: string; file_type_path?: string; file_name?: string; file_name_path?: string; state_scope?: 'page' | 'app'; nav_params?: NavParam[]; payload_extras?: NavParam[]; property_key?: string; value?: any; value_expression?: string; on_success?: ComponentEventSubscription[]; on_error?: ComponentEventSubscription[]; error_field?: string; error_state_key?: string; success_message?: string; error_message?: string; validate_before_action?: boolean; validate_field_names?: string[]; action_name?: string; action_icon?: string; action_visible_condition?: string; drill_column?: string; } interface EruComponent { id: string; type: string; isNested?: boolean; nesting_type?: 'object' | 'array' | 'nested_object' | 'nested_array'; pageId?: string; index?: number; entityName?: string; /** * dataKey of the page_ref that hosts this component, when that page_ref is * itself inside a looped nested page. Prefixed onto the component's own * dataKey (see BaseComponent.getDataKey) so a nested_array loop inside one * record card reads THAT card's row — `0.txns.1` — instead of resolving * `txns.1` against the outer page. */ dataKeyPrefix?: string; properties: ResponsiveEruComponentProperties; styles: { classes: string; responsive_classes: ResponsiveClasses; responsive_styles: ResponsiveStyleProperties; custom: Record; }; events?: ComponentEventSubscription[]; validation_rules?: ValidationRule[]; children?: EruComponent[]; parent_id?: string; created_at?: string; updated_at?: string; } interface EruComponentProperties { name?: string; placeholder?: string; label?: string; alt?: string; href?: string; target?: string; disabled?: boolean; required?: boolean; readonly?: boolean; options?: any[]; identifier?: boolean; [key: string]: any; } interface ResponsiveEruComponentProperties { base?: EruComponentProperties; sm?: EruComponentProperties; md?: EruComponentProperties; lg?: EruComponentProperties; xl?: EruComponentProperties; '2xl'?: EruComponentProperties; } interface StyleProperties { padding?: number; margin?: number; gap?: number; width?: string | number; height?: string | number; background?: string; color?: string; border?: string | number; rounded?: string | number; shadow?: string; font_size?: number; font_weight?: string | number; text_align?: string; direction?: string; justify_content?: string; align_items?: string; wrap?: boolean; columns?: number; rows?: number; [key: string]: any; } interface ResponsiveStyleProperties { base?: StyleProperties; sm?: StyleProperties; md?: StyleProperties; lg?: StyleProperties; xl?: StyleProperties; '2xl'?: StyleProperties; } interface ResponsiveClasses { base?: string; sm?: string; md?: string; lg?: string; xl?: string; '2xl'?: string; } interface MasterDetailConfig { master_field: string; detail_field: string; relationship_type: 'one-to-many' | 'many-to-one' | 'one-to-one'; auto_open?: boolean; display_mode?: 'popup' | 'side_panel'; } interface PageContainerConfig { page_id: string; display_mode: 'popup' | 'side_panel' | 'inline'; width?: string; height?: string; title?: string; closable?: boolean; resizable?: boolean; data_context?: Record; } interface PageNavigationEvent { page_id: string; action: 'open' | 'close' | 'navigate'; data?: any; config?: PageContainerConfig; } interface PageData { entity_id: string; entity_data: Record; [key: string]: any; } /** * A field-level write to one record, for other views of that record to apply. * * `set-page-data` gives the page a copy of the record, so an edit made through * the page does not reach the grid the copy came from. This carries just the * fields that moved, keyed by the record they belong to; `seq` distinguishes * repeat writes of the same value. */ interface RecordFieldPatch { entity_id: string; fields: Record; seq: number; } interface DynamicDataRequest { requestId: string; componentId: string; type: string; payload: any; status: 'pending' | 'success' | 'error'; error?: any; } interface EntityField { _is?: string; _rpst?: string; _rst?: string; allow_days?: string[]; api_field?: string; api_name?: string; can_edit?: boolean; can_group?: boolean; ce?: string; cef?: string; cefa?: string; close_status?: string; data_length?: string; data_length_check?: string; datatype?: string; date_format?: string; decimal?: number; def?: string; default?: string; default_group?: boolean; description?: string; dfc?: boolean; df_fields?: { def: string; dpef: string; }[]; dpef?: string; editable?: boolean | string; end_value?: any; entity_name?: string; entity_tab_name?: string; field_name?: string; field_size?: number; field_type?: 'SYSTEM' | 'USER'; fno?: number; grid_index?: number; is_ephi?: boolean; is_hidden?: boolean; is_pf?: boolean; is_pii?: boolean; is_unique?: boolean; label?: string; linked_obj?: any[]; mandatory?: boolean; map_roles?: string[]; map_users?: string[]; name?: string; nic?: string; nop?: string; num_val?: any; num_val_check?: string; open_status?: string; option_type?: string; options?: any[]; org_id?: string; org_process_id?: string; process_id?: string; process_name?: string; rich_text?: boolean; seperator?: string; show_grid?: 'yes' | 'no'; show_mobile?: boolean; start_value?: any; symbol?: string; system_validate?: string; ssv_api?: string; tab_name?: string; tf_idx?: number; to_encrypt?: boolean; tool_tip?: string; unique_fn?: any[]; unq_sa?: boolean; [key: string]: any; } interface Entity { display_name?: string; name?: string; fields?: EntityField[]; nested?: boolean; /** * Whether this entity has an approval matrix defined. Derived and persisted * by the backend when a matrix is saved, and shipped on the entity in the * process metadata — not something a client computes. */ has_am?: boolean; /** Whether a workflow is bound to this entity's APPROVE / REJECT trigger. */ has_wf_am?: boolean; /** * Whether a workflow is bound to any of this entity's other activity * triggers (SAVE / EDIT / DELETE / PREUPLOAD / POSTUPLOAD). */ has_wf_oth?: boolean; [key: string]: any; } interface DesignerState { current_page: EruPage | null; selected_component: EruComponent | null; drag_state: DragState; viewport: ViewportState; history: HistoryState; ui: UIState; } interface DragState { is_dragging: boolean; dragged_component?: ComponentDefinition; drop_target?: DropTarget; drag_preview?: DragPreview; } interface DropTarget { component_id: string; position: 'before' | 'after' | 'inside'; index?: number; } interface DragPreview { x: number; y: number; width: number; height: number; } interface ViewportState { breakpoint: TailwindBreakpoint; zoom: number; show_grid: boolean; show_rulers: boolean; preview_mode: boolean; } interface HistoryState { can_undo: boolean; can_redo: boolean; current_step: number; max_steps: number; } interface UIState { left_panel_open: boolean; right_panel_open: boolean; selected_tab: 'components' | 'layers' | 'assets'; property_tab: 'properties' | 'styles' | 'advanced'; show_welcome: boolean; loading: boolean; error: string | null; json_editor_visible?: boolean; json_editor_content?: string; json_editor_title?: string; } interface PageStatePatch { upsert?: Record[]; delete?: string[]; } interface PagePatch { page_id?: string; page_props?: Record; upsert?: Record[]; delete?: string[]; state?: PageStatePatch; } interface PagePatchResult { page: EruPage; warnings: string[]; added_state_keys: string[]; } /** A copy of the components with runtime-only event keys removed. */ declare function stripRuntimeEventKeys(components: EruComponent[]): EruComponent[]; declare function stripMountedPages(components: EruComponent[]): EruComponent[]; interface ScannedPageComponent { sequence?: number; source?: string; id: string; type?: string; parent_id?: string; children_ids?: string[]; component: Record; } declare function buildStagingTree(scanned: ScannedPageComponent[]): EruComponent[]; declare function applyPagePatch(base: EruPage | null, patch: PagePatch | null | undefined): PagePatchResult; /** * Snapshot of one page's runtime state, parked on the root store while the * page is unmounted so a return visit (Back, or navigating back to it) can * pick up where the user left off. See `retainPageState`. */ interface RetainedPageState { state: Record; instance_state?: Map>; page_data?: PageData; page_data_array?: PageData[]; } declare class EruStudioStore { private componentRegistry; private routeService; private injector; private _logicEvaluator; private logicEvaluator; private _entities; private _currentPage; private _selectedComponent; private _pageData; private _recordFieldPatch; private _recordFieldPatchSeq; private _queryData; private _pageDataArray; private _componentEventEmitted; private _fieldVisibilityOverrides; private _componentLoadingStates; private _componentVisibilityOverrides; private _fieldDisabledOverrides; private _componentPropertyOverrides; private _componentActions; private _componentDisabledStates; private _sidePanelOpenStates; private _clipboard; private _dynamicData; private _dynamicDataRequests; private _dynamicDataResponses; /** * Cache for session-stable dynamic-data responses, keyed by `${type}::${payloadKey}`. * Consulted on new requests of CACHEABLE_DYNAMIC_DATA_TYPES so that sibling * components asking for the same metadata (e.g. multiple grids reading * `ds_fields` for the same entity) get an instant response instead of * round-tripping the host handler again. */ private _dynamicDataCache; /** * requestId of the still-unanswered request per coalesce key, so identical * concurrent asks share one round-trip. Distinct from `_dynamicDataCache`, * which holds ANSWERED responses for types stable enough to reuse for the * session: this only ever merges requests that are in flight at the same * moment, so it cannot serve anything stale. */ private _dynamicDataInflight; private _pendingData; private _pageState; private _pageInstanceState; private _pageStateInitials; /** * Pages whose state is still being seeded by the controls on them. * * A page load is not one state write but a burst of them: every control that * declares a Default From State writes its seed value as it mounts, one per * change-detection pass. Anything that re-runs on a state change — a grid * that resolves bound variables into its query payload above all — would * otherwise fire once per control, in the same few hundred milliseconds, and * the answers land interleaved on top of each other. * * So the page is marked as settling the moment its state is initialised and * stays that way until the writes stop (see `schedulePageSettled`). A reader * that gates on `isPageSettling()` sits the burst out and runs ONCE, with the * final seeded values — while a later change, the user actually choosing * something in a control, passes straight through. */ private _pagesSettling; private pageSettleTimers; /** Latest moment a page is allowed to stay 'settling', however busy it is. */ private pageSettleDeadlines; /** Quiet period after the last seeding write before a page counts as loaded. */ private static readonly PAGE_SETTLE_QUIET_MS; /** Hard cap on the settling window, so a page that writes state on a timer * (a clock, a poll) cannot hold its grids empty forever. */ private static readonly PAGE_SETTLE_MAX_MS; private _runtimeContext; /** * Resolves through the parent chain: the host pushes the context onto the * outer store only, so a `nav_outlet` mount (whose inner store the host never * sees) has to inherit it — otherwise `@state.org_id` and friends read as * undefined inside every nested page. Own values win, so a store that is * given its own context is not overridden by its parent's. */ readonly runtimeContext: Signal>; static readonly SYSTEM_STATE_KEYS: string[]; setRuntimeContext(ctx: { org_id?: string; process_id?: string; process_name?: string; org_process_id?: string; user_id?: string; user_access?: any; }): void; private static readonly APP_STATE_STORAGE_KEY; private static readonly APP_STATE_MAX_BYTES; private _appState; private _appStateContext; /** * The top of the parent chain. App state and retained page state live here * so they outlive the inner stores that nav_outlet creates and destroys. */ rootStore(): EruStudioStore; /** Reactive view of app state — resolves through the parent chain to the root. */ readonly appState: Signal>; private _pendingNavParams; setPendingNavParams(pageId: string, params: Record): void; private takePendingNavParams; getAppStateValue(key: string): any; setAppStateValue(key: string, value: any): void; setAppStateValues(patch: Record): void; clearAppState(): void; private hydrateAppState; private persistAppState; private enforceAppStateContext; private _navSeq; private _pageNavigationRequest; readonly pageNavigationRequest: Signal<{ page_id: string; param_name: string; params?: Record; seq: number; } | null>; requestPageNavigation(page_id: string, param_name?: string, params?: Record): void; clearPageNavigationRequest(): void; /** * Run a `navigate-to-page` action. Shared by component events (base-component) * and page-level events (a custom signal a nested page emitted to its parent), * so both resolve the target the same way: a nav_outlet in the store chain gets * a route-param write, a standalone page hands the request to the host app. * * `evaluate` lets the caller supply its own expression evaluator — a component * inside a row/card scope has a context-bound one that resolves `@` refs * against that row; without it the page-level evaluator is used. */ navigateToPage(sub: ComponentEventSubscription, originPageId: string, dataKey?: string, evaluate?: (expression: string) => any): void; /** * The values a fired event can offer the page it navigates to, keyed by the * name the target would declare them under. * * Sources, lowest precedence first: * - `entity_data` — the record the event fired on (a drilled row's columns) * - `drill.column_name` / `drill.column_value` — which cell was clicked * - `extras` — what the author added via Extra Payload, so it can override * a same-named row field deliberately * * Nothing here is filtered by name: `routeSeedForPage` takes only the keys the * target page declares as state variables, which is the whole point — the * handover carries everything and the target decides what it wants. */ private autoParamsFromPayload; /** * Build the extra query params a `navigate-to-page` action carries to its * target. Each param name should match a state variable on the target page, * which `initPageState` seeds from the URL on mount. * * Values are stringified because query params are strings; the target coerces * back using its declared initial as the type hint. Objects need an explicit * `encode: 'json'` — silently JSON-ing an object here would produce * "[object Object]" in the URL for anyone who passed one by accident. */ resolveNavParams(sub: ComponentEventSubscription, pageId: string, dataKey?: string, evaluate?: (expression: string) => any): Record; private _childStores; readonly childStores: Signal; registerChildStore(store: EruStudioStore): void; unregisterChildStore(store: EruStudioStore): void; private _parentStore; setParentStore(store: EruStudioStore | null): void; getParentStore(): EruStudioStore | null; private readonly _outlets; registerOutlet(paramName: string): void; unregisterOutlet(paramName: string): void; hasOutlet(paramName: string): boolean; /** Walks this store and its ancestor chain for an outlet claiming paramName. */ hasOutletInChain(paramName: string): boolean; private static readonly RETAINED_PAGE_LIMIT; private readonly _retainedPageState; private _retainPageState; /** Enabled per canvas by `nav_outlet`'s `retain_page_state` property. */ setRetainPageState(retain: boolean): void; getRetainPageState(): boolean; /** * Snapshot a page's state onto the root store. No-op unless retention is on, * so pages that should always start clean are unaffected. */ capturePageStateSnapshot(pageId?: string): void; /** * Read and remove a page's snapshot. Consumed rather than kept so a later * re-init (e.g. a full page def replacing a stub) does not clobber values * the user has changed since the mount. */ private takeRetainedPageState; clearRetainedPageState(pageId?: string): void; private _dragState; private _hoveredComponentId; private _viewport; private _editMode; private _history; private _undoStack; private _redoStack; private _ui; readonly entities: Signal; readonly currentPage: Signal; readonly selectedComponent: Signal; readonly dragState: Signal; readonly viewport: Signal; readonly history: Signal; readonly ui: Signal; readonly editMode: Signal; readonly hoveredComponentId: Signal; readonly componentEventEmitted: Signal>>; readonly pageData: Signal<{ key: string; value: PageData; }[]>; readonly pageDataArray: Signal<{ key: string; value: PageData[]; }[]>; /** * The most recent field write to a page record, for views holding the same * record to apply. Carries a `seq` so two writes of the same value to the * same field are still two distinct notifications. */ readonly recordFieldPatch: Signal; readonly queryData: Signal>; /** @deprecated Use computedPageState instead. Will be removed in a future version. */ readonly derivedValues: Signal>; readonly fieldVisibilityOverrides: Signal>>; readonly componentLoadingStates: Signal>; readonly componentVisibilityOverrides: Signal>; readonly fieldDisabledOverrides: Signal>>; readonly componentPropertyOverrides: Signal>>; readonly componentActions: Signal>; readonly componentDisabledStates: Signal>; readonly sidePanelOpenStates: Signal>; readonly clipboard: Signal; readonly dynamicDataRequests: Signal>; /** * Own responses merged with the parent's, so a component inside a nested * canvas reads the answer to a relayed request from its own store — which is * the only store it knows about. */ readonly dynamicDataResponses: Signal>; /** * Own pending data merged with the parent's, for the same reason * `dynamicDataResponses` merges: a relayed request is answered on the ROOT * store — the one the host watches — and `removeDynamicDataRequest` parks it * in that store's pendingData. A grid inside a nested canvas matches its * request out of pendingData, so without the merge it never sees the answer * and renders empty. */ readonly pendingData: Signal>; readonly pageState: Signal>>; /** * One `@token` inside a state variable's `expr` formula. * * A flat lookup on the state map, which is what this was, could only see a * whole variable: `@selected_fi.entity_data._in` found no key of that literal * name, fell to the `?? 0` default, and the derived variable sat at 0 while * looking correctly configured. Dotted paths now walk (parsing a JSON-string * segment on the way, so a variable holding stringified JSON resolves too), * and the `state.` / `page.` / `app.` prefixes are accepted so an expression * reads the same here as it does anywhere else. * * Deliberately NOT LogicEvaluatorService: that reads computedPageState(), and * this IS computedPageState — going through it would recurse. App state and * page data are separate signals, so reading those here is safe. */ private readonly _warnedFormulaTokens; private warnUnresolvedFormulaToken; private warnFormulaFailed; private resolveFormulaToken; readonly computedPageState: Signal>; /** * Returns the computed page state for any pageId. For the current outer page * this delegates to `computedPageState()` (which applies formulas). For nested * pages — whose page def is not held in the global store — returns the manual * pageState map keyed by `pageId`. Used by logic-evaluator for `@state.X` * lookups so that nested components see their own page's state, not the outer. */ getComputedPageStateFor(pageId: string, dataKey?: string): Record; private linkedParentState; private isStateKeyLinkedToParent; private _agentStagingComponents; readonly agentStagingActive: Signal; readonly currentPageComponents: Signal; readonly selectedComponentProperties: Signal; readonly isDesignerReady: Signal; readonly nestedEntities: Signal; readonly childEntitiesMap: Signal>; readonly page: { current_page: Signal; current_page_components: Signal; selected_component: Signal; current_breakpoint: Signal; }; constructor(); setQueryData(key: string, value: any): void; getQueryData(key: string): any; getPageData(pageId?: string): PageData | null; getPageRecord(pageId?: string, dataKey?: string): PageData | null; readonly getPages: Signal<{ [k: string]: PageData; }>; getPageFieldData(fieldName: string, pageId?: string): any; setEntities(entities: Entity[]): void; /** * Whether the host has handed over the process's entity metadata yet. * * A condition on `@entity.*` cannot be answered before this flips, and * answering it anyway reads every attribute as undefined — which hides the * very components the condition was meant to reveal. Consumers show a ghost * for that window instead of committing to an answer. */ private _entitiesLoaded; readonly entitiesLoaded: Signal; /** One entity's metadata by name, for `@entity.*` / `@entities..*`. */ getEntity(entityName: string): Entity | null; getEntities(): Entity[]; getEntityFields(entityName: string): EntityField[]; getEntityField(fieldName: string, entityName?: string): EntityField | null; private _pageEntityNames; registerPageEntityName(pageId: string, entityName: string): void; getPageEntityName(pageId: string): string; private _nestedPageComponents; registerNestedPageComponents(pageId: string, components: any): void; getChildEntities(parentEntityName: string): Entity[]; /** Guards the page-level data-source effect against re-entrant re-apply. */ private _pageDataSourceKey; /** * The page whose declared record source has been applied to page data. * * `on_load` events send the page's record as their payload, so firing them * before the record lands sends `entity_id: ''` and a query filtered on it * comes back empty. That is invisible in the page viewer — the host seeds the * record as it mounts the page — but a page mounted in a `nav_outlet` gets * its record from this store's own state effect, which lands a tick later, so * on_load won the race and the page loaded against no record at all. */ private _pageRecordSourceApplied; /** * Walk a dotted path into a state value. JSON-string segments are parsed on * the way through and a JSON-looking result is parsed too, matching how * page_ref resolves `state_result_path`. `[n]` and `[*]` work as they do in * `resolvePayloadPath`. */ private resolveStatePath; /** Split a state value into { entity_id, entity_data }, as page_ref does. */ private normalizeStateRecord; setPageData(pageData: PageData, pageId?: string): void; /** * Give a page its record identity — the `set-page-data` action. * * A page-scoped save/fetch/delete sends `getPageData(pageId)` as its payload, * so a page whose selected record was only mirrored into a state variable * saves with a blank `entity_id` and the host writes a NEW row instead of * editing the selected one. Rather than mapping an id into every control's * save subscription (value-change subscriptions are per control), this writes * the identity once onto the page and every downstream action reads it. * * Source is `record_source`: 'event' (default) is the record the event itself * carries — a grid row select hands over the selected row — while * 'state'/'app_state' read the variable named by `state_key`. `record_path` * drills into whichever one is chosen. * * A resolved object is taken as the whole record ({entity_id, entity_data} or * a flat record with an `entity_id`/`id`); a scalar is taken as the id alone * and leaves the existing `entity_data` in place, which is the shape a state * variable like `program_id` holds. */ /** * Carry page-local field values across a record swap. * * Selecting a grid row replaces the page's RECORD. But a control that is not * a field of that record — a search box, a filter toggle — keeps its value in * the same `entity_data` bag, and a wholesale replace silently dropped it. * The control then re-synced itself to blank, which the value-change path * announced as a user edit, so a search box wiped its own state variable on * every keystroke and the search could never be typed. * * Only keys that the incoming record does not define AND that are not fields * of the page's entity are carried over. A genuine record field the new row * leaves unset must still read as absent, or the previous row's value would * leak into the one now on screen. */ private withPageLocalFields; /** * `download-file`: pull a base64 file out of an API response, state or page * data and save it through the browser. * * `response` is the natural source — the action hangs off an API's On * Success and the file came back as a JSON attribute. On an event with no * response behind it (a button click, a page signal) that same source reads * the event payload, so a file already in the payload needs no extra hop * through state. */ downloadFileFromAction(sub: ComponentEventSubscription, pageId?: string, response?: any, dataKey?: string): void; applyPageRecordSource(sub: ComponentEventSubscription, pageId?: string, fallbackRecord?: any): void; clearPageData(pageId?: string): void; /** * Reset all page data and state across the entire store. Used when the user * switches to a different upstream entity (e.g. fetches a new company on * step 1) and every nested page's grid/state needs to start fresh. * * Clears: pageData entity_data for every page, pageDataArray for every page, * per-instance state overlays, page-level state (re-seeded from initials), * field/component visibility & disabled overrides, and component loading * states. Does NOT touch the current page def itself. */ clearAllPageData(): void; updatePageData(pageData: Partial, pageId?: string): void; getPageDataArray(pageId?: string): PageData[]; readonly getPagesDataArray: Signal<{ [k: string]: PageData[]; }>; setPageDataArray(data: PageData[], pageId?: string): void; private readonly _serverValidationNotices; readonly serverValidationNotices: Signal; setServerValidationNotice(notice: ServerValidationNotice): void; removeServerValidationNotice(id: string): void; /** * Dynamic-data requests made on a nested canvas are relayed to the parent * store, and its answer is mirrored back here. * * A nav_outlet mount gets its own isolated store, but the host app only * watches the OUTER store's `dynamicDataRequests` — `collectCanvasStores()` * walks the tree for component events and nothing else. So a request issued * inside a nested page (a people field asking for the user directory, a * select asking for its options) landed in a map no one reads and was never * answered: the control just stayed empty. * * Relaying here rather than teaching the host to walk the tree keeps the * contract a library should have — ask your host, and let it ask its own — * and fixes every request type at once instead of one handler at a time. * Depth is unbounded: each level forwards to its own parent until the root, * which is the one the host watches. */ private readonly _relayedResponseSources; requestDynamicData(componentId: string, type: string, payload?: any): string; setDynamicDataResponse(request: DynamicDataRequest, data: any): void; /** * Returns a stable cache key for session-stable dynamic-data types, or * `null` for types that must always round-trip. * * Conservative whitelist: only entity-schema metadata, which is fixed * for the duration of a process session. Page defs and page lists * can change (user saves a page, creates a new one) so they round-trip; * if/when we add invalidation hooks we can grow this list. */ private getDynamicDataCacheKey; /** * Types whose identical concurrent requests should share a single round-trip, * keyed by `${type}::${payload}`. Unlike `getDynamicDataCacheKey` this implies * nothing about how long a response stays valid — the merge lasts only while * the request is unanswered — so a type belongs here as soon as many * components can ask for it at once, whether or not it is cacheable. */ private getDynamicDataCoalesceKey; /** Drop the cached response for a given (type, payload). Use when the * underlying source has changed mid-session — e.g. after the user saves * a page, invalidate `ds_page::`. */ invalidateDynamicDataCache(type: string, payload?: any): void; /** Drop the entire metadata cache (e.g. on org/project switch). */ clearDynamicDataCache(): void; removeDynamicDataRequest(requestId: string): void; removePendingData(requestId: string): void; handleDynamicDataError(requestId: string, error: any): void; getDynamicDataResponse(requestId: string): any; setDynamicData(key: string, value: any): void; getDynamicData(key: string): any; getComponentDynamicDataRequests(componentId: string): Map; setComponentEventEmitted(componentId: string, eventType: string, eventSubscription: ComponentEventSubscription): void; getComponentEventEmitted(componentId: string, eventType: string): ComponentEventSubscription[] | undefined; removeComponentEventEmitted(componentId: string, eventType: string): void; /** * Path segments, with bracket notation folded into dots and `[*]` kept as a * wildcard segment. * * A query result is commonly an array of row objects while a consumer — a * select's option list, a chip list — needs an array of ONE key from those * rows. `[*]` projects the rest of the path across every element to produce * exactly that: `Results.0.upl_file_metadata[*].file_name` turns the rows * into an array of file names. */ private static splitPath; private resolvePayloadPath; /** * `dataKey` scopes the read to one row of an array-nested page_ref — the same * row its components read and write. Without it a looped panel resolved * `state:` against page-level state (never seeing the per-row instance state * its own events had written) and `page:` against the page record instead of * the row, so every field it sent was empty or belonged to a sibling. */ resolveApiPayloadFields(fields: string[] | undefined | null, pageId?: string, dataKey?: string): Record; processExternalEvent(componentId: string, eventType: string, pageId?: string, payload?: any): void; processApiResponseActions(actions: ComponentEventSubscription[], pageId?: string, dataKey?: string, apiResult?: any): void; setFieldVisibilityOverride(pageId: string, fieldNames: string[], visible: boolean): void; setComponentLoadingState(componentIds: string[], isLoading: boolean): void; getComponentLoadingState(componentId: string): boolean; setComponentVisibilityOverride(componentIds: string[], visible: boolean): void; setFieldDisabledOverride(pageId: string, fieldNames: string[], disabled: boolean): void; getFieldDisabledState(pageId: string, fieldName: string): boolean; setComponentPropertyOverride(componentIds: string[], propertyKey: string, value: any): void; getComponentPropertyOverride(componentId: string, propertyKey: string): any; dispatchComponentAction(componentIds: string[], action: string, data?: any): void; consumeComponentAction(componentId: string): { action: string; data?: any; } | undefined; /** * `payload` is the emitting component's event payload (entity_id / * entity_data / entity_name), forwarded so a page event handling the signal * can read it — that is what `set-from-payload` resolves against. */ emitToParentPage(signalName: string, childPageId: string, payload?: any): void; /** * Whether a page's `on_load` events may fire yet. * * False only while a page that DECLARES a state record source is still * waiting for it: those events carry the record as their payload, so firing * early sends an empty `entity_id`. A page with no such declaration is ready * immediately — its record, if any, comes from the host. */ isPageRecordSourceReady(page: EruPage | null | undefined): boolean; fireOnLoadPageEvents(page: EruPage): void; private processPageEvents; /** * An action's configured value, with `value_expression` winning over the * static `value` — the same precedence base-component applies, so an action * reads the same whether it runs off a component event or an API response. */ private resolveSubscriptionValue; /** * Resolve and write an `update-state` action's value. `payload` is the event * payload the action fired with — the source for `set-from-payload`. */ private applyUpdateStateAction; setComponentDisabledState(componentIds: string[], disabled: boolean): void; getComponentDisabledState(componentId: string): boolean; setSidePanelOpenState(componentId: string, open: boolean): void; getSidePanelOpenState(componentId: string): boolean; setClipboard(component: EruComponent | null): void; findComponentById(id: string): EruComponent | null; findParentComponent(childId: string): EruComponent | null; private findParentInTree; setCurrentPage(page: EruPage | null, pageId?: string): void; pageForAgent(): EruPage | null; setAgentStagingComponents(scanned: ScannedPageComponent[] | null): void; clearAgentStaging(): void; applyAgentPatch(patch: PagePatch): PagePatchResult | null; private normalizeComponents; updatePage(updates: Partial): void; undo(): void; redo(): void; setSelectedComponent(component: EruComponent | null): void; addComponent(component: EruComponent, containerId?: string, index?: number): void; updateComponent(componentId: string, updates: Partial): void; deleteComponent(componentId: string): void; removeComponent(componentId: string): void; updateComponentProperty(componentId: string, propertyName: string, value: any): void; updateComponentPropertyAtBreakpoint(componentId: string, propertyName: string, value: any, breakpoint: TailwindBreakpoint | 'base'): void; getComponentProperty(name: string, breakpoint?: TailwindBreakpoint | 'base'): any; clearComponentPropertyAtBreakpoint(componentId: string, propertyName: string, breakpoint: TailwindBreakpoint | 'base'): void; updateStyleProperty(componentId: string, propertyName: string, value: any): void; clearStylePropertyAtBreakpoint(componentId: string, propertyName: string, breakpoint: TailwindBreakpoint | 'base'): void; updateStylePropertyAtBreakpoint(componentId: string, propertyName: string, value: any, breakpoint: TailwindBreakpoint | 'base'): void; renameStylePropertyAtBreakpoint(componentId: string, oldKey: string, newKey: string, breakpoint: TailwindBreakpoint | 'base'): void; switchLayoutType(componentId: string, newLayoutType: 'flex' | 'grid'): void; addComponentToContainer(containerId: string, component: EruComponent): void; moveComponentToContainer(componentId: string, targetContainerId: string): void; moveComponentToIndex(componentId: string, targetIndex: number): void; moveComponentBeforeTarget(sourceId: string, targetId: string): void; updateResponsiveClasses(componentId: string, breakpoint: TailwindBreakpoint | 'base', value: any): void; updateComponentStyle(componentId: string, styleName: string, value: any): void; setCustomStyle(componentId: string, key: string, value: any): void; renameCustomStyleKey(componentId: string, oldKey: string, newKey: string): void; removeCustomStyle(componentId: string, key: string): void; setCurrentBreakpoint(breakpoint: string): void; setDragState(dragState: Partial): void; startDrag(draggedComponent: any): void; endDrag(): void; setHoveredComponentId(componentId: string | null): void; setViewport(viewport: Partial): void; setBreakpoint(breakpoint: TailwindBreakpoint): void; setZoom(zoom: number): void; setUI(ui: Partial): void; togglePanel(panel: 'left' | 'right'): void; setSelectedTab(tab: UIState['selected_tab']): void; setPropertyTab(tab: UIState['property_tab']): void; setError(error: string | null): void; /** * Runtime effects of page events — property/visibility/disabled overrides and * loading flags. They describe what a page did while someone drove it, never * what it was authored as, so they are dropped whenever the page restarts or * the designer is entered or left. Left in place they read as the saved value: * a `set-property` fired in view mode kept painting the canvas after the * switch back to edit, hiding the property the page actually stores. */ private clearRuntimeOverrides; toggleEditMode(): void; setEditMode(mode: boolean): void; setLoading(loading: boolean): void; findComponentInTree(components: EruComponent[], componentId: string): EruComponent | null; private flattenComponentTree; private moveComponentBeforeTargetInTree; private addToContainer; private updateComponentInTree; /** * A declared state variable's starting value. * * Literal in every case except a date anchor (`@current_date - 7`, * `@first_day`), which is resolved at mount so a page can start from a date * relative to today — the same thing the date control's Default Date offers a * field, expressed the same way. * * Deliberately narrow: only a recognised anchor is evaluated, so an initial * that merely happens to start with '@' stays the literal string it was. * Full expression support would also be near-useless here — initials are * resolved before page data exists, so a `@field` reference has nothing to * read. */ private resolveStateInitial; /** * Apply a declared Initial Value to the running page state now. * * initPageState deliberately lets an existing runtime value win per key, so a * re-render (or a stub page def arriving before the full one) never discards * what the user or an action put there. That rule is wrong for an authoring * edit: changing Initial Value in the designer is an explicit instruction to * use the new value, and without this the panel kept showing the old one * until the page was saved and reloaded. Date anchors resolve here too, so * `@current_date - 7` shows its date the moment the variable is saved. */ applyStateInitial(key: string, initial: any, pageId?: string): void; /** * True while `pageId`'s controls are still seeding its state on load. * * Read as a signal, so an effect that returns early on a settling page * re-runs by itself the moment the page is settled and can then do its work * with the finished state. */ isPageSettling(pageId?: string): boolean; /** * Mark a page as loading and start the quiet-period countdown. Called when * the page's state is initialised — i.e. before any control has mounted. */ beginPageSettle(pageId: string): void; /** * (Re)arm the quiet-period timer. Each seeding write pushes the end of the * window back, so the page is declared loaded only once the writes stop — * but never past the deadline `beginPageSettle` set. */ private schedulePageSettled; /** Keep the current page's load window open for an app-state write. */ private extendPageSettleForCurrentPage; /** Declare the page loaded: gated readers re-run from here on. */ private endPageSettle; initPageState(page: EruPage, pageId?: string): void; /** * Values for this page's declared state variables that are present as URL * query params — how a `navigate-to-page` action's `nav_params` reach the * target page. The URL is the transport precisely because it crosses the * store boundary a nav_outlet mount creates, and survives refresh and Back. * * Only declared state keys are read, so unrelated query params (`page`, * `view`, host app params) never leak into page state. */ private routeSeedForPage; /** * Query params are always strings. Coerce back using the declared initial as * the type hint, so a numeric state var doesn't silently become a string — * and leave everything else alone (ids like "007" or "12345678901234567890" * must survive verbatim). */ private decodeRouteParamValue; /** Put back the data half of a retained snapshot (state is merged by the caller). */ private restoreRetainedPageData; getPageStateValue(key: string, pageId?: string, dataKey?: string): any; /** * Write a runtime state value. * * `declare: false` writes the value but leaves the page's declared `state` * list alone. A caller that writes purely to show something on the canvas — * a chip list seeding its default selection while the page is being edited — * must not add a state variable to the page as a side effect, or opening a * page in the designer would dirty it. */ setPageStateValue(key: string, value: any, pageId?: string, dataKey?: string, options?: { declare?: boolean; }): void; processApiResult(apiResult: any, subscription: { on_success?: ComponentEventSubscription[]; on_error?: ComponentEventSubscription[]; error_field?: string; error_state_key?: string; success_message?: string; error_message?: string; }, pageId?: string, dataKey?: string): { isError: boolean; errorMessage: string; }; /** * `eventRecord` is the payload of the event that asked for the write — for a * value change, the field that changed and its new value. It is consulted * only where page data has no answer, which is the case that made a * state-bound control wipe the very variable it had just set: the control * keeps its value in page STATE, so `set-from-field` read page data, found * nothing, and wrote undefined over it. */ updateStateWithFormula(key: string, formula: UpdateStateFormula, pageId?: string, dataKey?: string, scope?: 'page' | 'app', recordOverride?: { entity_id: string; entity_data: Record; } | null, eventRecord?: { entity_id?: string; entity_data?: Record; } | null): void; /** * Single entry point for state writes so every `update-state` call site * honours `state_scope` without repeating the branch. */ writeStateValue(key: string, value: any, scope: 'page' | 'app' | undefined, pageId?: string, dataKey?: string): void; private removeComponentFromTree; static ɵfac: _angular_core.ɵɵFactoryDeclaration; static ɵprov: _angular_core.ɵɵInjectableDeclaration; } declare class ComponentRegistryService { private components; private categories; private loadedComponents; constructor(); private initializeCategories; /** * Register a new component definition */ registerComponent(definition: ComponentDefinition, component?: Type): void; /** * Register multiple components at once */ registerComponents(definitions: ComponentDefinition[]): void; /** * Get a component definition by type */ getComponent(type: string): ComponentDefinition | undefined; /** * Get all registered components */ getAllComponents(): ComponentDefinition[]; /** * Get components by category */ getComponentsByCategory(category: ComponentCategory): ComponentDefinition[]; /** * Get all categories with their components */ getAllCategories(): Map; getLoadedComponent(type: string): Type | undefined; private extractComponentEvents; /** * Get component events for a specific component type */ getComponentEvents(componentType: string): { label: string; value: string; }[]; /** * Search components by name or type */ searchComponents(query: string): ComponentDefinition[]; /** * Validate if a component can be a child of another component */ canBeChild(childType: string, parentType: string): boolean; /** * Get components that can be parents for a given component type */ getPossibleParents(componentType: string): ComponentDefinition[]; /** * Get components that can be children of a given component type */ getPossibleChildren(parentType: string): ComponentDefinition[]; /** * Validate component hierarchy depth */ validateNestingDepth(components: any[], maxDepth?: number): boolean; getComponentClassAsync(type: string): Promise | undefined>; static ɵfac: _angular_core.ɵɵFactoryDeclaration; static ɵprov: _angular_core.ɵɵInjectableDeclaration; } declare class EruApiService { private baseUrl; constructor(); uploadImage(file: File): Observable<{ url: string; }>; static ɵfac: _angular_core.ɵɵFactoryDeclaration; static ɵprov: _angular_core.ɵɵInjectableDeclaration; } declare const BASIC_COMPONENTS: ComponentDefinition[]; declare const LAYOUT_COMPONENTS: ComponentDefinition[]; declare const FORM_COMPONENTS: ComponentDefinition[]; declare const ERU_COMPONENTS: ComponentDefinition[]; declare const NAVIGATION_COMPONENTS: ComponentDefinition[]; declare const DATA_COMPONENTS: ComponentDefinition[]; declare const LOADING_COMPONENTS: ComponentDefinition[]; declare const ALL_COMPONENTS: ComponentDefinition[]; declare class PropertyToTailwindService { convertFlatStylePropertiesToResponsive(flatProps: StyleProperties): ResponsiveStyleProperties; generateClassesFromStyleProperties(styleProps: ResponsiveStyleProperties): ResponsiveClasses; convertStylePropertiesToCSS(styleProps: Record): Record; isRenderableStyleKey(key: string): boolean; private getCSSProperty; private propertyMap; generateClassesForBreakpoint(properties: Record): string; generateResponsiveClasses(properties: ResponsiveStyleProperties): ResponsiveClasses; combineAllClasses(styles: { classes: string; responsive_classes: ResponsiveClasses; }): string; getPropertyValueForBreakpoint(properties: ResponsiveStyleProperties, propertyKey: string, breakpoint: TailwindBreakpoint | 'base', useFallback?: boolean): any; private getFontSizeClass; private getFontWeightClass; private getTextAlignClass; private getJustifyClass; private getAlignClass; private pxToSpacing; private getColorClass; private getBackgroundColorClass; static ɵfac: _angular_core.ɵɵFactoryDeclaration; static ɵprov: _angular_core.ɵɵInjectableDeclaration; } declare class LogicEvaluatorService { private store; /** * Date anchors usable in any expression, with an optional offset in days: * `@current_date`, `@current_date - 7`, `@first_day + 1`. * * The vocabulary deliberately mirrors the date control's Default Date * anchors (Current Date / First Day of Month / Last Day of Month, plus an * offset in days), so the same idea is expressed the same way whether it is * configured on a field or written in an expression. * * The optional trailing `± n` is part of the match so it is CONSUMED by the * substitution. Left behind, `@current_date - 7` would first become a date * and then have 7 subtracted from it as a number — arithmetic on a * timestamp, not on days. */ private static readonly DATE_KEYWORDS; /** True when a value uses one of the date anchors above. */ static hasDateKeyword(value: any): boolean; /** * Replace each date anchor (and its day offset) with a quoted ISO date, so * everything downstream sees an ordinary date literal. In a value * expression that yields the date itself; in a condition it lands on the * existing ISO-literal handling, which turns both sides into timestamps so * the comparison is chronological rather than lexical. */ static resolveDateKeywords(expression: string): string; /** Midnight-anchored date for a keyword, so no time component leaks in. */ private static isTruthy; private static anchorDate; /** * Evaluates a logic expression string. * Example: "@firstName == 'John' && (@age > 18 || @isAdmin == true)" * @param expression The expression string * @param pageId Optional page ID for context * @returns boolean result of the evaluation */ /** * Does this expression read entity metadata? Used by callers that must not * answer a condition before `setEntities` has run — every attribute would * read undefined and the condition would settle on the wrong answer. */ static referencesEntityMetadata(expression: any): boolean; /** * Resolve one `@token` to its value. Shared by evaluate() and * evaluateValue(), which each used to carry their own copy of this chain — * so a namespace added to one silently did not exist in the other. * * Namespaces, in precedence order: * `state.x` page state * `app.x` window-wide app state * `page.x` page data by key * `entity.x` attribute of THIS page's entity * `entities` every entity in the process, as an array * `entities..x` attribute of a named entity * `column.x` / `row.x` the record in scope (board card / event row) * bare `x` row field if present, else page data */ private resolveToken; evaluate(expression: string | any, pageId?: string, componentId?: string, dataKey?: string, rowData?: Record | null): boolean; /** * Depth counter rather than a boolean: `evaluate` can be re-entered (a * token resolver reading state that itself evaluates), and a boolean would * be cleared by the inner call returning. */ private strictDepth; /** * Evaluate, distinguishing "false" from "could not be evaluated". * * `evaluate` resolves any failure to `true`, which is right for layout — a * mis-typed visibility condition should not blank part of a page. It is * wrong wherever the condition is hiding something on purpose, because * then a typo reveals it. Callers that need to fail closed use this and * treat `null` as "hide". */ evaluateStrict(expression: string | any, pageId?: string, componentId?: string, dataKey?: string, rowData?: Record | null): boolean | null; /** * Resolve a state reference like `program_data.sts` against the computed * page state. First tries an exact (possibly dotted) key for backward * compatibility, then walks the dotted path into nested objects — parsing a * JSON-string segment if it encounters one (state values are sometimes * stored as stringified JSON). The resolved leaf is parsed too, so a * variable holding `'["a","b"]'` reaches an API payload as an array rather * than as a quoted string. */ private resolveStatePath; evaluateValue(expression: string, pageId?: string, dataKey?: string, rowData?: Record | null): any; static ɵfac: _angular_core.ɵɵFactoryDeclaration; static ɵprov: _angular_core.ɵɵInjectableDeclaration; } interface FormControlInfo { componentId: string; fieldName: string; control: FormControl; } interface CustomValidator { name: string; validator: ValidatorFn; } declare class FormValidationService { private store; private tailwindService; private schemaService; private logicEvaluator; private forms; private formControls; private customValidators; readonly formValid: _angular_core.WritableSignal; readonly formErrors: _angular_core.WritableSignal>; /** * Bumped whenever a form-level check runs, so every field can refresh the * message it shows. * * A field's `` text comes from its own `validationErrors` signal, * which it refreshes from the control's `statusChanges`. A form-level check * marks controls touched and revalidates with `emitEvent: false` — touching * never emits a status change, and the flag suppresses the one revalidation * would raise — so a field the user never touched turned red (Material reads * the control directly) while its message stayed empty. A select is where * this showed most: a text box usually gets a keystroke and, with it, a * status change, while a dropdown left alone gets nothing. * * The flag itself stays: dropping it would emit `valueChanges` too, and the * subscription behind that writes page data and announces a value change — * pressing Save would fire every field's Value Change actions. */ private readonly _validationTick; readonly validationTick: _angular_core.Signal; constructor(store: EruStudioStore, tailwindService: PropertyToTailwindService, schemaService: ComponentSchemaService, logicEvaluator: LogicEvaluatorService); initializeFormForPage(pageId: string): void; registerFormControl(component: EruComponent, pageId: string): FormControl | null; unregisterFormControl(componentId: string, pageId: string): void; getFormControl(componentId: string, pageId: string): FormControl | null; getAllFormControls(pageId: string): FormControl[]; getAllFormControlInfos(pageId: string): FormControlInfo[]; getFormControlsByFieldName(pageId: string): Map; getForm(pageId?: string): FormGroup | null; validateForm(pageId: string): boolean; validateFields(pageId: string, fieldNames: string[]): boolean; private validateControlInfos; private isComponentEffectivelyVisible; private checkComponentVisibility; getFormErrors(pageId: string): Record; getControlErrors(control: AbstractControl): string[]; private getDefaultErrorMessage; private buildValidators; private getValidationRules; /** * Validation rules derived from the bound data-model field. * * The data model expresses these as metadata — `num_val` + `num_val_check` * for a numeric bound, `data_length` + `data_length_check` for a length — * rather than as component properties. Translating them into ValidationRules * here means the limits are enforced by the same machinery as hand-authored * rules, and a page picks them up purely by being bound to the field. The * component's own rules are appended after these so an explicit rule wins. */ private getDataModelValidationRules; private buildValidatorFromRule; private buildCustomValidator; registerCustomValidator(name: string, validator: ValidatorFn): void; unregisterCustomValidator(name: string): void; private updateFormStatus; private getPropertyValue; private getOrCreateParentGroup; private getLeafName; static ɵfac: _angular_core.ɵɵFactoryDeclaration; static ɵprov: _angular_core.ɵɵInjectableDeclaration; } /** * Whether a string is a usable website address. * * Deliberately accepts a bare host (`example.com`) as well as a full URL: the * website component itself prepends `https://` when it builds the href, so * rejecting the bare form would fail values the component handles correctly. * A scheme, when present, must be http or https — `javascript:` and `data:` * are never a website, and one of them is an injection vector. */ declare function isValidWebsiteUrl(value: string): boolean; /** * Row-scoped data context provided by board-card hosts and consumed by descendant * components (text / chips / progress_bar / …) when they resolve `value_source: 'field'`. * * Each rendered card creates its own instance via component-level `providers`, * so multiple cards on screen don't trample each other's data. * * BaseComponent injects this token as `optional` — when no row context exists * (i.e. rendering outside a board card), it falls back to the page data store. */ declare class RowDataContext { readonly row: _angular_core.WritableSignal; readonly columns: _angular_core.WritableSignal; readonly group: _angular_core.WritableSignal; /** * Look up `fieldName` against the current row. * Tries `row.entity_data[name]` first (eru-grid's standard shape), * then a flat `row[name]` lookup as a fallback. * * A dotted name is a path into the row: `pj.0.fa` reads element 0 of the * row's `pj` array. That is what a page_ref looping an array field inside a * cell/board card hands us — its cards carry a data key of `.` * and the array only exists on the row, never in page data. */ getFieldValue(fieldName: string): any; /** * Walk a dotted path from the row. Numeric segments index arrays, and a * segment holding JSON text is parsed before the walk continues — a query * can return an array column as a string. */ private resolvePath; private static parseIfJson; /** * Write `fieldName` back into the current row, mirroring getFieldValue's * precedence so a value is read back from where it was put. * * An editable control inside a card reads its value from here but wrote its * edit only to page data, which a card never reads — so the moment the * control returned to view mode it redisplayed the untouched row value and the * edit looked discarded. The row is replaced rather than mutated so the signal * notifies and the card re-renders. */ setFieldValue(fieldName: string, value: any): void; hasRow(): boolean; /** * The effective row data object (entity_data merged over the flat row), for * resolving `@field` references in logic expressions against the card's row. */ getData(): Record | null; /** * The row in page-record shape, so actions that read `$entity_data` / * `$entity_id` (update-state, emit-to-parent payloads) resolve the card's own * row instead of the card page's shared page data — a board card never writes * its row into the store, so there is nothing there to read. * * eru-grid rows are flat (fields plus `entity_id`), so the flat row itself is * the record data; a row already carrying `entity_data` is passed through. */ getRecord(): { entity_id: string; entity_data: Record; } | null; static ɵfac: _angular_core.ɵɵFactoryDeclaration; static ɵprov: _angular_core.ɵɵInjectableDeclaration; } /** * Reads and writes URL query parameters as a reactive signal. * * Used by app-shell components (`nav_menu` writes a param to switch * the active page; `nav_outlet` reads it to mount the right page). * * Query params are chosen over path segments because: * - Refresh-safe out of the box (Angular keeps query string). * - No host-side route registration needed for each nav target. * - Multiple shells can coexist on a route by using different param names. * * The service is provided in root so all components on the page share * the same view of the URL — including nested page mounts where each * has its own EruStudioStore but the URL itself is global. */ declare class EruAppRouteService { private router; private destroyRef; private _params; /** All current query params as a plain object. */ readonly routeParams: _angular_core.Signal>>; constructor(); /** Read a single param by name. Empty string when absent. */ getParam(name: string): string; /** Reactive accessor — recomputes when the URL changes. */ paramSignal(name: string): _angular_core.Signal; /** * Write or clear a single param. * * Pass `null`/empty to remove the param. Other params are preserved. * * Implementation note: building a `UrlTree` against `router.url` * preserves the current path verbatim regardless of which child route * we're under. We can't use `relativeTo: ActivatedRoute` because this * service is `providedIn: 'root'` and the root ActivatedRoute doesn't * carry the current child segments. */ setParam(name: string, value: string | null): void; /** Bulk set/clear. Same semantics as `setParam` per key. */ setParams(patch: Record): void; private applyQueryParams; private refreshFromSnapshot; static ɵfac: _angular_core.ɵɵFactoryDeclaration; static ɵprov: _angular_core.ɵɵInjectableDeclaration; } type NegativeDisplay = 'as_is' | 'colored' | 'colored_abs' | 'parentheses'; /** * Record an event carries instead of the emitting component's page data — used * when the event is about one row rather than the page (grid row select, grid * cell edit). `row_entity_name` and `old_entity_data` are set only by row-level * edits, and are what let the host app save the row's own entity. * * `row` is the untouched source row. A cell edit's `entity_data` is only the one * field that changed, so without the row a handler has no access to the rest of * the record — the ids, the query aliases, the columns it wants to send along. */ interface EventPayloadOverride { entity_id?: string; entity_data?: any; entity_name?: string; row_entity_name?: string; old_entity_data?: any; row?: any; /** * Extra top-level keys to merge onto the subscription payload, for event * detail that is not part of the record. A drill click needs to say which * cell was clicked and what its value was, and none of the record-shaped * fields above can carry that. * * Namespace what you put here (the drill event uses a single `drill` key) so * an author's payload path stays stable and cannot collide with the * record fields. Reaches handlers as `eventSubscription.payload`, so it is * addressable from `set-from-payload` and goes out with a call-query/function. */ context?: Record; } interface StyleSchema { key: string; type: 'color' | 'number' | 'select' | 'boolean' | 'text'; label: string; category: 'layout' | 'style' | 'typography' | 'spacing' | 'border' | 'background'; default_value: any; placeholder?: string; options?: Array<{ label: string; value: any; }>; min?: number; max?: number; step?: number; width?: 1 | 2 | 'half' | 'full'; } declare class BaseComponent implements OnInit, OnDestroy { protected store: EruStudioStore; /** * The breakpoint scope of the nearest enclosing page reference or runtime * canvas, when this component is rendered inside one. Absent on the * designer's top-level canvas, where the toolbar selection is authoritative. */ private breakpointScope; /** * The breakpoint every responsive read below resolves against. * * A scoped breakpoint is measured from the container this component actually * sits in, so a page opened in a side panel lays out for the panel's width * instead of the browser window's. Where nothing provides a scope this is the * store's viewport, exactly as before. */ protected currentBreakpoint(): TailwindBreakpoint | 'base'; protected schemaService: ComponentSchemaService; protected tailwindService: PropertyToTailwindService; protected formValidationService: FormValidationService; protected logicEvaluator: LogicEvaluatorService; protected routeService: EruAppRouteService; protected rowContext: RowDataContext | null; protected elementRef: ElementRef; protected renderer: Renderer2; protected injector: Injector; protected formControl: FormControl | null; private _syncHydrated; private _syncedRecordId; private _syncWasVisible; private validationErrorsSignal; protected readonly validationErrors: _angular_core.Signal; checkVisibility(component: EruComponent): boolean; /** * True while this component's visibility hangs on entity metadata that has * not arrived. Drives the same shimmer as an explicit loading state. */ protected readonly isAwaitingEntityMetadata: _angular_core.Signal; protected readonly isComponentVisible: _angular_core.Signal; protected readonly isAncestorChainVisible: _angular_core.Signal; protected readonly isEffectivelyVisible: _angular_core.Signal; protected readonly isComponentMandatory: _angular_core.Signal; protected readonly effectiveDefaultValue: _angular_core.Signal; /** The state variable this field seeds from and writes back to, if configured. */ protected readonly defaultStateKey: _angular_core.Signal; /** Which state that variable lives in. */ protected readonly defaultStateScope: _angular_core.Signal<"page" | "app">; /** * Current value of the state variable named by 'Default From State'. * * Read as a signal so the seeding effect re-runs when the variable is * populated later — a page whose state is filled by a query or a nav param * would otherwise seed from an empty value on mount and never correct itself. */ protected readonly defaultStateValue: _angular_core.Signal; /** * Mirror an edit back into the bound state variable. * * Deliberately additive: the normal page-data write still happens, so the * field keeps taking part in save-page-data exactly as before. This is the * difference from Value Source = state, which routes the value to state * INSTEAD of page data. */ protected writeBackToDefaultState(value: any): void; /** * The value a form control should hold for a given stored value — the single * place the control is fed from, so overrides cannot end up fighting the sync * effect. Override to show something when page data holds nothing (e.g. the * date component's resolved Default Date). Returning undefined leaves a * freshly created control untouched and clears an existing one. */ protected controlValueFor(value: any): any; /** Override in a component that derives its default from more than the raw * property (e.g. the date component's Current Date / First Day / offset). */ protected resolveDefaultValue(): any; protected readonly isComponentDisabled: _angular_core.Signal; protected readonly isComponentLoading: _angular_core.Signal; /** * Whether double-clicking the view-mode display would actually open the * editor — the same three conditions the display's dblclick handler checks. * * Templates bind this to drive the hover affordance, so the tint and pointer * appear only where an edit is really possible. They used to show on every * control, promising an editor that a read-only or disabled field never opens. */ protected readonly canInlineEdit: _angular_core.Signal; protected readonly inlineEditHoverBg: _angular_core.Signal; /** * How a display-mode value behaves when it is wider than its slot. Wrapping is * the default because that is what these controls have always done; the other * two exist for fixed-height layouts (board cards especially), where a value * spilling onto a second line pushes everything below it out of place. */ protected readonly textOverflowMode: _angular_core.Signal<"wrap" | "ellipsis" | "clamp">; protected readonly textOverflowStyles: _angular_core.Signal>; /** The component's own styles with the overflow behaviour merged on top. * A hidden component is left alone — clamp sets `display`, which would * otherwise undo `display: none`. */ protected readonly stylesWithTextOverflow: _angular_core.Signal>; protected textOverflowPropertySchema(): PropertySchema[]; protected readonly isHiddenByEvent: _angular_core.Signal; protected readonly isDisabledByEvent: _angular_core.Signal; private eventListeners; getDataKey(component: EruComponent): string; dataKey: _angular_core.Signal; /** * Signal-backed input for the component definition. * Using @Input with a setter to avoid InputSignal brand mismatch issues in library consumers. */ readonly component: _angular_core.WritableSignal; set componentSetter(value: EruComponent); componentEvents: { label: string; value: string; }[]; excludeBaseEvents: string[]; targetElementSelector: string; eventTargets: { [event: string]: string; }; protected getEventTargets(): { [event: string]: string; }; protected readonly classes: _angular_core.Signal; protected readonly customStyles: _angular_core.Signal; protected readonly computedStyles: _angular_core.Signal>; /** * Custom styles are merged LAST, because the panel promises they bypass the * responsive style system. With the responsive styles last a custom value was * silently beaten by anything the Styles tab also set for that property. * * Keys are normalised to kebab-case on the way in. The responsive system emits * kebab (`letter-spacing`) while the panel asks authors for camelCase * (`letterSpacing`), so the two spellings are different object keys: both * survived the merge and whichever ngStyle happened to write last won. */ protected readonly combinedStyles: _angular_core.Signal; protected getProperty(key: string): any; /** * Read a property whose key was renamed to match the data model, falling back * to the pre-rename key so pages saved earlier keep their configured value. * New edits always write the current key, so a page migrates the first time * the property is touched. * * Both reads deliberately bypass the schema default (passing `undefined`): * getProperty() substitutes the default when nothing is stored, which would * always beat the legacy value and make the fallback dead code. */ protected getPropertyWithLegacy(key: string, legacyKey: string): any; /** * The bound entity field's colour bands, when the data model defines them. * The model is the single place bands are authored — the same rule a select's * options follow — so every page and the grid paint a value identically. */ protected modelColorRanges(): ColorRange[] | null; /** * `disable_if` helper for a `color_ranges` property: the panel editor is * locked while the bound data-model field supplies the bands, and stays open * when it does not (unbound component, or a field with no bands configured). */ protected hasModelColorRanges: (getValue?: (k: string) => any) => boolean; /** * Label → colour for a field whose data model enumerates its values. * * One helper for both shapes the model uses: a tag or select field lists its * values under `options`, a status field splits them across `open_status` and * `close_status`. Which one is present decides how the field is read — a * shape test rather than a datatype test, so `r_status` (which is in neither * library's datatype union) resolves its colours like any other status field. * * Each entry carries `name` when it came from the model and `label` when a * property panel authored it; both are accepted. * * `fieldName` defaults to this component's own bound field. Pass another * field's name to borrow its palette — a chip list summarising a status * column needs that column's colours, not its own. * * Shared because tag and status each hand-rolled this parsing, which is how * one field ends up painted two different ways by two controls. */ protected modelOptionColors(fieldName?: string): Map; /** * Black or white, whichever stays legible on the given background. * Returns black for anything it cannot parse, which is the safer default * against the light surfaces the studio paints by default. */ protected contrastTextColor(background: string): string; /** * Value for a property the data model owns: the bound field's definition wins, * and the component's own property is the fallback for an unbound component. * * This is the general form of the rule `default_value`, `color_ranges` and the * select's option source already follow. Reach for it whenever the key is in * the inherited set — resolving from the component's stored property alone * means a field configured in the data model renders one way on a page and * another way in a grid, which is the whole class of bug this exists to close. */ protected modelOrProperty(key: string, legacyKey?: string): any; /** * The inverse of `modelOrProperty`: the data model SEEDS the value and the * component's own property overrides it. * * For keys the data model has an opinion about but a page may legitimately * disagree with on one component — an attachment's upload destination, where * the field says where files normally go and a particular page files them * somewhere else. `modelOrProperty` cannot express this: the model always * wins there, so an override would be silently ignored. */ protected propertyOrModel(key: string): any; /** * The component's display label, resolved the way the properties panel shows * it: `label` is a data-model owned key (INHERITED_FIELD_KEYS), so a bound * field's label wins and the component's own property is the fallback for an * unbound one. Reading the stored property alone is why a bound field's panel * read "Deal Status" while the page rendered no label at all — the panel never * copies an inherited value into the page. */ protected readonly resolvedLabel: _angular_core.Signal; /** Decimal precision for a numeric field, from the data model's `decimal`. * Falls back to 0 — an unset `decimal` means an integer, the same reading * eru-grid's cell renderer gives it. A component that wants a different * fallback (currency, conventionally 2) carries it as its schema default. */ protected numericDecimals(): number; /** * Display formatting shared by the number and currency components: precision * from `decimal`, digit grouping from `seperator`, and optional abbreviation. * * Shared so the two cannot drift on how the same value renders, and so * `seperator` — which the data model has always carried, and which eru-grid * has always honoured — finally applies on a page too. Previously both * components hardcoded the en-US grouping, so an Indian-format column * formatted one way in a grid and another way on a page. */ protected formatNumericValue(value: any): string; /** * How this control renders a negative value. A component property rather than * `modelOrProperty`: the data model has no such attribute, so a lookup there * only ever returns undefined and then falls through to the same property. */ protected negativeDisplayMode(): NegativeDisplay; /** * The colour a negative value is painted, or null when it is not one or the * chosen mode does not paint. * * The question is asked of the VALUE, not of the formatted text: 'no sign' * and 'brackets' have already removed the minus by the time there is a string * to look at, so a renderer that sniffed the text would paint neither. */ /** * A colour the current VALUE dictates, or null when nothing about the value * has an opinion. Overridden by the numeric controls, which answer with the * matching Colour Range's colour or the negative colour. * * Read inside `combinedStyles`, so the override lands on the same style map * as the author's Text Color instead of racing it from a separate binding — * `[style.color]` on an element ngStyle also writes is settled by whichever * ran last, which is how a styled component kept its text colour on negatives. */ protected valueColorOverride(): string | null; protected negativeColorFor(value: any): string | null; /** * An abbreviation scale only if one was actually chosen — the bound field's, * else the one stored on this component. * * Deliberately not `modelOrProperty`, which falls through to `getProperty` * and so would hand back the schema's `default_value: 'mn'`. Since choosing a * scale is itself what turns abbreviation on (see formatNumberValue), that * default would read as a choice on every numeric field and abbreviate the * lot — every plain number rendering as '98.98 k'. */ private explicitAbbreviationScale; /** * The field's value in the representation page data keeps — the one that ends * up in the save-entity payload. * * Paired with `get_formatted_value()` so the two readings of a value can never * be confused for each other. A control that formats for display (number, * currency) must render through that one, because everything a formatter adds * — a currency symbol, digit grouping, a fixed precision — makes the value a * string, and writing that string back is how a numeric column comes to be * stored as text. Anything downstream that aggregates or compares the column * then does so on text: `sum()` fails outright and `max()` answers * lexicographically ('98975' > '145000'). */ get_value(): any; /** * Layout for a view-mode display label. * * Applied to the label SPAN rather than to its container: every display * template is `
…` * with the value straight after, so a block label already puts the value on * the next line — where restyling the container would have to reckon with * what each one holds (chips, avatars, a multi-line list). * * `margin-right` is cleared with it, since the 6px that separates a label * from a value beside it is dead space above one. */ readonly displayLabelStyles: _angular_core.Signal>; /** * Typography the author set for the LABEL specifically, from the `label_*` * style keys the Styles tab writes when its Typography group is switched to * the Label target. * * Kept as its own set of keys rather than inherited from the component shell: * the shell's typography is the VALUE's, and a label that inherited it could * not be a caption without also pinning every property in CSS — which is * exactly the half-pinned state that made Text Color reach only the value * while Font Weight reached both. Unset keys are simply absent here, so the * shared `.eru-display-label` caption rule remains in charge of them. */ protected readonly labelTypographyStyles: _angular_core.Signal>; /** * Layout for the view-mode display CONTAINER that holds label and value. * * Only `flex-direction` is set, never `display`: most of these containers are * already a flex row, and the few that are not (date, number, currency) are * plain blocks where the block label above is what stacks them — forcing flex * on those would drop the `text-align` a number column depends on. * * 'left' returns nothing at all rather than restating the row direction, so * the layout there is byte-for-byte what it was before this property existed. */ readonly displayContainerStyles: _angular_core.Signal>; /** * Typography that must land on the VALUE's own element rather than on the * component shell. * * Only the properties that cannot be scoped from the shell live here. * `text-decoration` is the case: it is drawn across all in-flow descendants * and a descendant cannot remove it, so set on the shell it underlined the * label too. Everything else the Styles tab sets stays on the shell and * inherits, with the label opting out via the shared caption rule. */ readonly displayValueStyles: _angular_core.Signal>; /** * The value span's own styles plus the overflow rules, for the controls whose * template already bound `textOverflowStyles()` there. Angular templates have * no spread, so the merge belongs here rather than in the binding. */ readonly valueTextStyles: _angular_core.Signal>; /** The same value as the control shows it. Identity unless a control formats. */ get_formatted_value(): string; /** * A numeric control's value as page data should keep it. See * `parseNumberValue` for why a non-numeric entry is passed through untouched. * * Held here rather than in each numeric component so the number and currency * fields cannot drift on it, the same reason `formatNumericValue` is shared. */ protected numericStoreValue(value: any): any; protected matchColorRange(value: any, key?: string): ColorRange | null; protected setProperty(key: string, value: any): void; protected getStyleProperty(key: string): any; private hasRequiredRule; protected hasRequiredValidation(): boolean; readonly fieldValue: _angular_core.Signal; protected supportsValueSource(): boolean; protected readonly valueSourceMode: _angular_core.Signal<"field" | "state">; /** * Which state map a state-bound field reads and writes. * * App state is shared across pages and survives navigation, and its keys are * not declared anywhere — so the key comes from a free-text property while * page state keeps its picker of declared variables. */ protected readonly valueStateScope: _angular_core.Signal<"page" | "app">; /** The state variable this field binds to, in whichever scope is selected. */ /** Guards the state-default seeding below to one write per field. */ private defaultStateSeeded; protected readonly boundStateKey: _angular_core.Signal; /** The state map behind `boundStateKey`, for reads and for value_path patching. */ protected boundStateMap(): Record; /** * Read `path` out of a (possibly JSON-string) value. * * `[n]` indexes an array and `[*]` projects the rest of the path across every * element — `rows[*].file_name` turns an array of row objects into an array * of file names. A wildcard path is read-only: `setValueByPath` has no * meaningful inverse for it, so a field that writes back should not use one. */ private resolveValueByPath; /** * Write `value` at `path` inside `target`, returning a new object — the * inverse of resolveValueByPath. * * Needed because several fields commonly bind to ONE state variable, each * reading its own path out of it (seven toggles on `program_data`, paths * `sec_enabled` / `etf` / `ca` / ...). Writing the bare value to the variable * replaced that whole object with a single boolean, after which every other * field's path resolved against a non-object and fell back to the raw value — * so flipping one toggle flipped all of them. * * A JSON string is re-serialised so the state variable keeps the shape it had. */ private setValueByPath; protected readonly resolvedStateValue: _angular_core.Signal; protected readonly storeValue: _angular_core.Signal; /** * Page-data key for a field on the SAME record as this component. * * `storeValue()` prefixes the component's own field with `dataKey()`, which is * what makes a component inside an array-nested page_ref read its own row * (`"0.fa"`). Any read of a *different* field on that record has to do the * same: an unprefixed name has no record to resolve against, and * `getPageFieldData` answers it with `records.map(d => d.entity_data)` — the * whole array of rows. That is where the stray `[object Object]` came from: * a currency's symbol field and a tile's value field both asked for a bare * name and got the row array, which stringifies to exactly that. */ protected scopedFieldKey(fieldName: string): string; /** * A field's value read off the card's row, scoped the way page data is. * * The scoped key comes first for the same reason `storeValue` prefixes page * data with it: inside a looped record card the component means its OWN * record (`pj.0.fa`), and the row may well carry a field of the same bare * name. The bare lookup stays as the fallback — that is every component * directly on a card, where there is no data key at all. */ protected rowFieldValue(fieldName: string): any; /** * Current value of a SIBLING field, resolved the same way as this component's * own value — row data first when rendered inside a board card, then * page data under the same data key. * * Needed for a dependent dropdown: `dpef` names another field on the same * record whose value scopes this field's option list, so the list cannot be * requested without reading that field. */ protected siblingFieldValue(fieldName: string): any; /** * Run the subscriptions wired to an event. * * `subscriptionFilter` narrows the matches beyond the event name, for events * where several subscriptions of the same type coexist and only one of them * is meant to fire. A grid's 'Custom Action' is the case: every configured * row action is its own `custom_action` subscription, and the click says * which action it was — without the filter, clicking one icon would run * every action on the grid. */ protected emitEvent(eventType: string, data?: any, defaultSubscription?: ComponentEventSubscription, payloadOverride?: EventPayloadOverride, subscriptionFilter?: (subscription: ComponentEventSubscription) => boolean): void; private processEventSubscription; /** * Resolve a subscription's `payload_extras` into a flat {key: value} map. * * Each entry names a payload key and an expression evaluated in this * component's scope: `@state.fd`, `@app.x`, `@page.x`, and a bare `@column` * for a field of `record` — the record the event fired on, which for a drill * is the clicked row. That last one is why `record` is passed in rather than * read from the row context: the row context is a board-card thing, and a * grid has none. * * Returns undefined when nothing is configured, so the payload keeps its * previous shape for every event that does not use this. */ /** Expression prefixes that identify a path, so one written without '@' is * still recognisable as an expression rather than a literal. */ private static readonly EXPR_NAMESPACE; private resolvePayloadExtras; private resolveActionValue; /** * Convert a form control's value into the representation page data keeps. * Inverse of `controlValueFor`, and identity unless a control's editor works * in a different type than the one that is stored — the datepicker hands back * a `Date` where the store holds a `yyyy-MM-dd` string. * * Without this the raw control value was written and announced: a date picked * as 20-08 reached subscribers as `2026-08-19T18:30:00.000Z`, the previous day * in any timezone east of UTC. */ protected storeValueFor(controlValue: any): any; /** * The field's value for anything a template renders — the stored value, with * the form control consulted only while the store holds nothing. * * `formControl.value` is a plain property, not a signal, so a computed that * read it never re-ran when the control changed: a display built on it kept * showing whatever the value was the first time it evaluated, and an edit made * in place appeared to revert the moment the control returned to view mode. * Reading `storeValue()` first makes the store the reactive source, and it is * kept current on every keystroke by the value-change path. */ protected reactiveFieldValue(): any; /** Last value announced, so one edit is never announced twice. */ private lastAnnouncedValue; /** * Keep the card's own row in step with an edit made inside it. * * `storeValue()` prefers the row context over page data whenever the component * renders inside a board card, so writing the edit only to page data left the * card still showing the old value. No-ops outside a card. */ protected writeRowValue(fieldName: string, value: any): void; /** * Announce a value change, once. * * A change can reach here twice for a single edit: the form control's * `valueChanges` fires when the editor writes it, and controls that own a * change handler (date's `dateChange`, checkbox, rating, …) also call * `onValueChange`. Both paths are needed — the ten controls without a handler * have only the subscription — so the duplicate is suppressed here instead. * Only a repeat of the value just announced is dropped; going A → B → A still * announces three times. */ private announceValueChange; /** Field and prior value of the change currently being announced, if any. */ private pendingOldValue; /** * Whether the change being announced is one the CONTROL made for itself — * 'Auto Select First Option' seeding an empty field — rather than a gesture * the author made on the designer canvas. * * Edit mode suppresses configured actions because a click or a toggle there * is authoring, not use. A self-seed is neither: it is the control doing at * design time exactly what it will do on load, and the page is built around * what follows from it — the dependent dropdown whose list is fetched for * the seeded parent. Suppressing it left the author looking at an empty * currency list with no way to fill it. */ private selfSeededChange; /** * Run a value write as a self-seeded change, so its Value Change actions * fire on the designer canvas too. See `selfSeededChange`. */ protected asSelfSeededChange(run: () => void): void; /** * Forget the last announcement, so re-applying the same value counts as a new * change. Needed where a write was lost rather than superseded — a default * seeded before the page's data load is wiped by it, and the re-seed has to * announce again or the actions listening to it never re-run. */ protected resetValueAnnouncement(): void; /** * Payload carrying only the field that changed, when the component asks for it * via `value_change_payload`. * * The default payload is the whole record — the card's entire row, or the * page's data — which is right for a save triggered by a Save button but wrong * for a single edit: it writes back every key it was given, including the * query aliases a board row carries (a computed `o_sn`, a `rsts` that is not * even the stored field), and it cannot say what the value used to be. * * Returning undefined leaves the existing record-shaped payload in place. */ private changedFieldPayload; protected onValueChange(newValue: any): void; /** * Write a value to wherever this field's value lives — the bound state * variable, or page data (and the enclosing card's row). * * Split out of `onValueChange` so a component can correct what the store * already holds without it looking like a user edit: the checkbox rewrites a * boolean `true` as the configured `value_true` ("Y") on load, and that must * not sync the control or announce a `valueChange` the author never made. */ protected writeStoreValue(newValue: any): void; constructor(); ngOnInit(): void; ngOnDestroy(): void; protected registerFormControl(): void; protected unregisterFormControl(): void; protected updateValidationErrors(): void; protected markControlAsTouched(): void; private ssvRequestId; private ssvRequestedValue; /** Server-side validation message for this field, or '' when none. */ readonly serverValidationMessage: _angular_core.WritableSignal; /** True when the last server-side check reported an error. */ readonly serverValidationFailed: _angular_core.WritableSignal; /** * Ask the host app to validate this field's value, when the bound data-model * field sets `system_validate`. * * Runs on focus-out. The request goes over the same dynamic-data channel as a * dropdown's option fetch — neither library makes HTTP calls, the host does — * under the type `ds_server_validation`. The host answers with * `{ status: 'success' | 'error', msg }`. * * The entered value is never reverted: the check reports, it does not correct. * Navigation is never blocked either, so a slow check cannot trap the user in * a field. */ protected requestServerSideValidation(): void; /** * Consume a server-side validation response. Called from an effect set up in * the constructor so every field component gets it without extra wiring. */ private consumeServerValidationResponse; /** * Register a dependency on a value control's view/edit mode. * * The nineteen value controls each declare their own `_mode` signal rather * than inheriting one, so it is read reflectively: a component that has one * contributes the dependency, and anything else contributes none. Only the * read matters — the value is unused. */ private readRenderedMode; protected setupEventListeners(): void; private getEvents; private addEventListeners; private getTargetElement; private removeAllEventListeners; protected getEventMap(): { [key: string]: (event: any) => void; }; protected onClick(event: any): void; protected onMouseEnter(event: any): void; protected onMouseLeave(event: any): void; protected onFocus(event: any): void; protected onBlur(event: any): void; protected onMouseDown(event: any): void; protected onMouseUp(event: any): void; protected onKeyDown(event: any): void; protected onKeyUp(event: any): void; protected createPropertyComputed(key: string, defaultValue?: any): _angular_core.Signal; protected createStylePropertyComputed(key: string, defaultValue?: any): _angular_core.Signal; protected getPropertyValue(component: EruComponent, key: string, defaultValue?: any): any; protected getStylePropertyValue(component: EruComponent, key: string, defaultValue?: any): any; protected getClasses(component: EruComponent): string; protected getCustomStyles(component: EruComponent): any; /** * The child's own custom styles that belong on its flex/grid item wrapper * rather than on the child element. Called by the containers when building a * child wrapper's styles; see ITEM_ONLY_STYLE_PROPERTIES for why. */ protected getCustomItemStyles(child: EruComponent): Record; protected getComputedStyles(component: EruComponent): Record; private getStylesForBreakpoint; private getPropertyValueForBreakpoint; protected getCommonPropertySchema(): PropertySchema[]; protected getCommonStyleSchema(): StyleSchema[]; protected resolvePayloadPath(payload: any, path: string): any; protected getExcludedProperties(): string[]; protected getExcludedStyles(): string[]; protected getComponentPropertySchema(): PropertySchema[]; protected getComponentStyleSchema(): StyleSchema[]; protected getComponentDefaultProperties(): EruComponentProperties; protected getComponentDefaultStyles(): StyleProperties; protected getPropertySchema(): PropertySchema[]; protected getStyleSchema(): StyleSchema[]; protected getDefaultProperties(): EruComponentProperties; protected getDefaultStyles(): StyleProperties; protected getAllProperties(): Record; readonly commonEvents: { label: string; value: string; }[]; /** * Whether an event must stay silent on the designer canvas. * * True for everything by default: a click or a toggle there is authoring, not * use. Override for an event that reports what the canvas is already showing * — the shell's initial menu selection, say, whose handler fills a header the * designer otherwise renders empty. */ protected suppressEventsInEditMode(eventType: string): boolean; private getComponentEvents; static ɵfac: _angular_core.ɵɵFactoryDeclaration; static ɵdir: _angular_core.ɵɵDirectiveDeclaration; } declare class ComponentSchemaService { private componentSchemas; private _registeredTypes; registerComponentSchema(componentType: string, propertySchema: PropertySchema[], styleSchema: StyleSchema[], componentEvents?: { label: string; value: string; }[]): void; getPropertySchema(componentType: string): PropertySchema[]; getStyleSchema(componentType: string): StyleSchema[]; getComponentEvents(componentType: string): { label: string; value: string; }[]; getAllSchemas(componentType: string): { propertySchema: PropertySchema[]; styleSchema: StyleSchema[]; }; getRegisteredComponentTypes(): string[]; isComponentRegistered(componentType: string): _angular_core.Signal; get registeredTypes(): _angular_core.Signal>; static ɵfac: _angular_core.ɵɵFactoryDeclaration; static ɵprov: _angular_core.ɵɵInjectableDeclaration; } /** * The breakpoint a subtree of components resolves its responsive properties * against, measured from the box they actually occupy rather than from the * browser window. * * A page reference renders its nested page inside whatever space the host gave * it — a side panel a few hundred pixels wide, a narrow inline slot in a * two-column layout — but every component in it read `store.viewport()`, which * is derived from `window.innerWidth`. A page opened in an 800px side panel on * a 1400px screen therefore laid itself out as `xl`: desktop columns, desktop * type, nothing wrapped. The store cannot answer this, and must not: the * nested page shares the host's store on purpose (page state, dynamic-data * wiring and events all flow through it), so the breakpoint is the one thing * that has to be scoped separately. * * Provided per page-reference and per runtime canvas, so the nearest enclosing * container wins and nesting resolves naturally. Components read it through * `BaseComponent.currentBreakpoint()`, which falls back to the store's viewport * wherever no scope is provided — the designer's top-level canvas, where the * toolbar's breakpoint selector is the author's deliberate choice. */ declare class BreakpointScopeService { private readonly _breakpoint; /** The scoped breakpoint, or null until a width has been measured. */ readonly breakpoint: _angular_core.Signal; setWidth(width: number): void; /** * Track an element's content-box width for as long as the returned teardown * is not called. Returns a no-op teardown where ResizeObserver is missing * (SSR, older embedded webviews), leaving the subtree on the store's * viewport rather than on a stale guess. */ observe(element: HTMLElement | null | undefined): () => void; static ɵfac: _angular_core.ɵɵFactoryDeclaration; static ɵprov: _angular_core.ɵɵInjectableDeclaration; } declare class TextComponent extends BaseComponent { constructor(); protected getComponentStyleSchema(): StyleSchema[]; protected getComponentDefaultProperties(): EruComponentProperties; protected getComponentDefaultStyles(): StyleProperties; protected getComponentPropertySchema(): PropertySchema[]; private resolveByPath; /** * Fall back to the configured Default Value when the bound value is missing. * A display-only component never gets the page-data seeding the input * components rely on — that effect is skipped on the designer canvas and * inside a board card row — so a text bound to a column the backend returns * as `null` ignored its default everywhere it mattered. */ private withDefault; readonly displayValue: _angular_core.Signal; /** * What the template prints. A whitespace-only value — typically a ' ' Default * Value set precisely to stop the box collapsing — renders as nothing in HTML: * normal white-space processing drops collapsible spaces at the start and end * of a line, leaving a line box with no height. Non-breaking spaces survive * it, so the blank line the author asked for actually appears. Page data still * receives displayValue(), so the stored value stays a plain space. */ readonly renderedValue: _angular_core.Signal; static ɵfac: _angular_core.ɵɵFactoryDeclaration; static ɵcmp: _angular_core.ɵɵComponentDeclaration; } declare class TileComponent extends BaseComponent { private readonly queryData; constructor(); /** The dynamic-data request this tile is waiting on for its query rows. */ private readonly queryRequestId; /** The query already asked for, so the effect does not re-fetch on every tick. */ private lastQueryRequested; /** * The single row a tile reads its *_field values from. * * A saved query answers with one envelope per statement, each keyed by its * result set: `[{ Results: [ …rows ] }]`. Reading `data[0][field]` off that * lands on the envelope, not the row, so every field came back undefined and * the tile showed nothing even once the query had run. */ private readonly queryRow; protected getComponentDefaultProperties(): EruComponentProperties; protected getComponentDefaultStyles(): StyleProperties; protected getComponentPropertySchema(): PropertySchema[]; /** * Value behind a configured field, with an optional dot path into it. * * A state variable (or a query/page-data field) often holds a JSON object — * the record a row select parked there, an aggregate returned as one blob — * so the field alone resolves to the whole thing and the tile has nothing to * show. `path` drills in, parsing JSON strings on the way, the same rule * page_ref's State Result Path follows. */ private resolveField; readonly resolvedVariant: _angular_core.Signal<"progress" | "card" | "metric" | "gauge">; /** Preset shell, or a saved page projected as the tile body. */ readonly resolvedLayout: _angular_core.Signal<"page" | "preset">; readonly cardPageId: _angular_core.Signal; /** * Which visual fills the value slot. This is the only thing `variant` decides * now — the header, title, subtitle, secondary value, sparkline and alert are * shared by every variant rather than re-declared (and half-forgotten) in each. */ readonly valueVisual: _angular_core.Signal<"number" | "bar" | "arc">; /** The arc draws its own value, so the shell must not draw it twice. */ readonly showShellValue: _angular_core.Signal; readonly density: _angular_core.Signal<"compact" | "comfortable" | "spacious">; readonly emphasis: _angular_core.Signal<"sm" | "md" | "lg" | "xl">; /** * Title before or after the value. Defaults per variant so existing pages keep * their reading order; set explicitly to make all variants agree. */ readonly labelPosition: _angular_core.Signal<"above" | "below">; /** `card` shows primary and secondary as two stats; the rest inline the secondary. */ readonly statsLayout: _angular_core.Signal<"row" | "inline">; readonly hasSecondary: _angular_core.Signal; /** * With `data_source: static` the *_value_label properties hold the value * itself, so rendering them again as a caption prints it twice. */ private readonly isStatic; readonly showValueLabel: _angular_core.Signal; readonly showSecondaryLabel: _angular_core.Signal; readonly hasIcon: _angular_core.Signal; readonly iconPosition: _angular_core.Signal<"left" | "top" | "right">; readonly iconSize: _angular_core.Signal; /** The header row exists for the badge, and for the icon only when it sits there. */ readonly showHeader: _angular_core.Signal; /** '' means "follow the value emphasis"; anything else pins a step on the scale. */ private textStep; readonly titleEmphasis: _angular_core.Signal; readonly subtitleEmphasis: _angular_core.Signal; readonly labelEmphasis: _angular_core.Signal; readonly shellClasses: _angular_core.Signal<{ [x: string]: boolean; 'tile--page': boolean; }>; readonly primaryValue: _angular_core.Signal; readonly secondaryValue: _angular_core.Signal; readonly formattedPrimary: _angular_core.Signal; readonly formattedSecondary: _angular_core.Signal; private modelFieldFor; private explicitSeperator; /** Resolve the currency symbol: from a field/state value if configured, else * the static symbol property. */ private resolveCurrencySymbol; /** Format a value for display. Only formats when dynamic numbering is on or a * currency symbol is set — otherwise returns the value unchanged so plain * text / counts are untouched. */ private formatValue; /** Negative colour for each value slot, or null. See BaseComponent. */ readonly negativePrimaryColor: _angular_core.Signal; readonly negativeSecondaryColor: _angular_core.Signal; readonly resolvedTitle: _angular_core.Signal; readonly resolvedBadge: _angular_core.Signal; readonly resolvedAlert: _angular_core.Signal; /** * The first colour rule whose band contains the primary value, or null. * * Split out of `dynamicColors` because the two answer different questions. * `dynamicColors` is "what colour is this tile", which folds in the plain * Text Colour as a fallback — and that fallback is not a rule about the value, * so it must not outrank the negative colour. Only a MATCH does. */ private readonly matchedColorRule; readonly dynamicColors: _angular_core.Signal<{ bg: any; text: any; }>; /** Text colour from a matched rule only — see `matchedColorRule`. */ readonly ruleTextColor: _angular_core.Signal; readonly tileStyles: _angular_core.Signal; readonly tileScale: _angular_core.Signal; readonly gaugeCircumference: number; readonly gaugeDashoffset: _angular_core.Signal; readonly gaugeColor: _angular_core.Signal; readonly progressPercent: _angular_core.Signal; readonly sparklinePath: _angular_core.Signal; readonly sparklineAreaPath: _angular_core.Signal; static ɵfac: _angular_core.ɵɵFactoryDeclaration; static ɵcmp: _angular_core.ɵɵComponentDeclaration; } declare class ButtonComponent extends BaseComponent { /** * If the button's click event toggles a side_panel/popup page_ref, return its target id. * Used by `isToggleActive`/`effectiveLabel`/`effectiveIcon` to flip label/icon based on * the target panel's open state. */ private readonly toggleTargetId; readonly isToggleActive: _angular_core.Signal; readonly effectiveLabel: _angular_core.Signal; readonly effectiveIcon: _angular_core.Signal; private readonly iconOnlyVariants; readonly effectiveTooltip: _angular_core.Signal; readonly iconStyles: _angular_core.Signal>; protected onButtonPress(event: any): void; protected onButtonRelease(event: any): void; protected onButtonHover(event: any): void; protected onButtonFocus(event: any): void; protected onButtonBlur(event: any): void; componentEvents: { label: string; value: string; }[]; protected getComponentPropertySchema(): PropertySchema[]; static ɵfac: _angular_core.ɵɵFactoryDeclaration; static ɵcmp: _angular_core.ɵɵComponentDeclaration; } declare class ImageComponent extends BaseComponent { private effectiveComponent; private getEffectiveProp; readonly src: _angular_core.Signal; readonly alt: _angular_core.Signal; readonly objectFit: _angular_core.Signal; readonly objectPosition: _angular_core.Signal; readonly loading: _angular_core.Signal; readonly fallbackIcon: _angular_core.Signal; readonly tooltip: _angular_core.Signal; readonly hasError: _angular_core.WritableSignal; private _prevSrc; onImageError(): void; onImageLoad(): void; protected getComponentPropertySchema(): PropertySchema[]; protected getComponentStyleSchema(): StyleSchema[]; protected getComponentDefaultProperties(): EruComponentProperties; protected getComponentDefaultStyles(): StyleProperties; static ɵfac: _angular_core.ɵɵFactoryDeclaration; static ɵcmp: _angular_core.ɵɵComponentDeclaration; } interface ToggleOption { label: string; value: string; icon?: string; } declare class ButtonToggleComponent extends BaseComponent implements OnInit { readonly toggleOptions: _angular_core.Signal; readonly displayMode: _angular_core.Signal<"icon_label" | "icon_only" | "label_only">; /** Extra class on the group, so the stylesheet can square up the buttons. */ readonly groupClasses: _angular_core.Signal; showIcon(option: ToggleOption): boolean; /** * Icon-only hides the text but NOT an option that has no icon to show in its * place — `toggle_icons` is matched positionally and a slot may be left empty, * and a button rendering nothing at all is unpressable in practice. Such an * option keeps its label so the group stays usable. */ showLabel(option: ToggleOption): boolean; /** * The label as a tooltip, for the mode that hides it. This is the only place * an icon-only option says what it means, so it is not decoration: without it * the option's meaning exists nowhere on screen. */ tooltipFor(option: ToggleOption): string; readonly defaultSelection: _angular_core.Signal; readonly currentValue: _angular_core.Signal; /** Set once the user picks a button, after which the default is never re-seeded. */ private userHasChosen; /** A click on the toggle — the user's choice replaces the default for good. */ onUserToggle(value: any): void; constructor(); protected getComponentPropertySchema(): PropertySchema[]; protected getComponentDefaultProperties(): EruComponentProperties; protected getComponentDefaultStyles(): StyleProperties; static ɵfac: _angular_core.ɵɵFactoryDeclaration; static ɵcmp: _angular_core.ɵɵComponentDeclaration; } declare class BadgeComponent extends BaseComponent { protected getComponentPropertySchema(): PropertySchema[]; protected getComponentDefaultProperties(): EruComponentProperties; private resolveByPath; readonly badgeValue: _angular_core.Signal; protected getComponentDefaultStyles(): StyleProperties; static ɵfac: _angular_core.ɵɵFactoryDeclaration; static ɵcmp: _angular_core.ɵɵComponentDeclaration; } /** One rendered segment of a chip — a single key's value, never concatenated. */ interface ChipPart { key: string; value: string; } interface ChipItem { /** Emitted on click and held as the selection. */ value: string; parts: ChipPart[]; /** Resolved colour, or null when nothing supplies one. */ color: string | null; } type SelectionMode = 'none' | 'single' | 'multi'; declare class ChipsComponent extends BaseComponent { componentEvents: { label: string; value: string; }[]; /** * The list's raw value, by authoring mode: * * - `static` — `chips`, a comma-separated string. * - `field` — `storeValue()`, which honours RowDataContext inside a board card. * - `state` — the named page state variable. Read here rather than through * `supportsValueSource()` because that routes a component's *edits* to * state; this list is a display whose selection goes somewhere else * entirely (`selection_state_key`), so binding both to one key would have * the selection overwrite the list it came from. */ private readonly rawValue; /** Keys to render, in order. Empty means every key the object carries. */ private readonly displayKeys; /** * Label → colour from the data model, for `color_source: model_field`. * * `color_field` names the field whose options carry the palette; it falls back * to this component's own bound field. A chip list is usually a summary of * some other column — the Deals stage counts are not themselves a `ds` value — * so the field being *coloured by* is rarely the field being *read*. */ private readonly optionColors; readonly chipList: _angular_core.Signal; private toChipItem; /** Numbers go through the shared numeric formatter so a count in a chip * groups its digits the same way the same number does anywhere else. */ private formatPart; private resolveColor; readonly selectionMode: _angular_core.Signal; /** State variable the selection is written to and read back from. */ private readonly selectionKey; /** * Selection held by the component itself, for when no state variable is named. * * Without this, turning Selection on but leaving Selection State Variable * empty produced a chip list that could be clicked and never highlighted — * the click was written nowhere, so nothing could read it back. */ private readonly localSelection; /** * Currently selected values. * * Read back out of the state variable rather than held locally whenever one is * named, so the highlight reflects what the rest of the page is actually * filtering on — including a selection cleared by some other control's * `update-state`. */ readonly selectedValues: _angular_core.Signal; isActive(chip: ChipItem): boolean; /** * Values that should start selected. * * A default, not a binding: it is read once to seed the selection and then * never consulted again. Holding it live would re-assert itself after every * click, and clearing the active chip is behaviour `nextSelection` goes out of * its way to allow — a filter the user cannot switch off is a trap. * * Static lists nominate values directly. Field/state lists nominate the key * carrying the flag, because the values are not known at authoring time. * Either way the answer is a plain list of chip values, so the seeding path * below does not care which mode produced it. */ private readonly defaultSelection; /** A flag from a query is as likely to arrive as 1 or 'true' as a boolean. */ private isSelectedFlag; /** Seeded once per mount, so a cleared selection is never re-seeded. */ private readonly defaultSeeded; /** * The default last applied, so an authoring edit re-seeds on the canvas. * Only consulted in edit mode — at runtime the seed stays a one-shot. */ private lastSeededDefaults; constructor(); /** * Colours for a chip. * * The resting look is eru-grid's status cell exactly — `statusPillColors` * lightens the fill 90% toward white, keeps the border saturated and darkens * the text 30% — so a chip, a status badge and a grid cell painted from the * same field are the same pill. Selecting a chip inverts it: the border colour * becomes the fill, and the light fill becomes the text. * * The colours travel as custom properties because they cannot be inherited: * Material's label element sets `color` on itself and sits between the host * and our spans, so both an inherited `color` and the * `--mdc-chip-label-text-color` token lose to it. A custom property passes * through it untouched and the stylesheet spends it deeper down. * * With no colour resolved this returns nothing, so an uncoloured chip list * keeps Material's default look. */ chipStyles(chip: ChipItem): Record; onChipClick(chip: ChipItem): void; /** * Clicking the active chip clears it, in both modes. A filter the user cannot * switch off is a trap — this is the behaviour a segmented toggle cannot give, * and the reason a filter belongs on chips rather than on one. */ private nextSelection; private writeSelection; private toSelectionArray; /** * Whether a click on a chip does anything — which is what the pointer cursor * promises. Selection is one reason; a wired Chip Click or Click subscription * is the other, since a chip list can drive an action while owning no * selection state of its own. */ readonly isClickable: _angular_core.Signal; /** The detail both chip events carry, so the two cannot describe a click differently. */ private chipContext; /** * `Click` carries the clicked chip too. * * The base binds one DOM listener for `click` on the host and emits with the * page-data payload, which has no idea a chip was involved — so an author who * reached for the familiar `Click` in the panel (rather than `Chip Click`) got * a subscription that fired with nothing to read, and a `set-from-payload` on * `chip.value` resolved to nothing. Resolving the chip from the event target * keeps that one listener and gives it the same context `chipClick` sends. */ protected getEventMap(): { [key: string]: (event: any) => void; }; /** The chip a DOM event happened inside, or null for the gaps between them. */ private chipFromEvent; protected getComponentPropertySchema(): PropertySchema[]; protected getComponentDefaultProperties(): EruComponentProperties; protected getComponentDefaultStyles(): StyleProperties; static ɵfac: _angular_core.ɵɵFactoryDeclaration; static ɵcmp: _angular_core.ɵɵComponentDeclaration; } declare class IconComponent extends BaseComponent { readonly iconName: _angular_core.Signal; private resolveByPath; readonly iconSize: _angular_core.Signal; readonly fontSet: _angular_core.Signal; readonly iconColor: _angular_core.Signal; readonly inline: _angular_core.Signal; readonly tooltip: _angular_core.Signal; readonly ariaLabel: _angular_core.Signal; readonly ariaHidden: _angular_core.Signal; protected getComponentPropertySchema(): PropertySchema[]; protected getComponentStyleSchema(): StyleSchema[]; protected getComponentDefaultProperties(): EruComponentProperties; protected getComponentDefaultStyles(): StyleProperties; static ɵfac: _angular_core.ɵɵFactoryDeclaration; static ɵcmp: _angular_core.ɵɵComponentDeclaration; } declare class ProgressBarComponent extends BaseComponent { private resolveByPath; readonly resolvedValue: _angular_core.Signal; readonly progressValue: _angular_core.Signal; readonly barColor: _angular_core.Signal; readonly trackColor: _angular_core.Signal; protected getComponentPropertySchema(): PropertySchema[]; protected getComponentDefaultProperties(): EruComponentProperties; protected getComponentDefaultStyles(): StyleProperties; static ɵfac: _angular_core.ɵɵFactoryDeclaration; static ɵcmp: _angular_core.ɵɵComponentDeclaration; } declare class ProgressSpinnerComponent extends BaseComponent { protected getComponentPropertySchema(): PropertySchema[]; protected getComponentDefaultProperties(): EruComponentProperties; protected getComponentDefaultStyles(): StyleProperties; static ɵfac: _angular_core.ɵɵFactoryDeclaration; static ɵcmp: _angular_core.ɵɵComponentDeclaration; } declare class TimerComponent extends BaseComponent implements OnDestroy { private intervalId; private _remaining; private _running; private _autoStarted; readonly remaining: _angular_core.Signal; readonly running: _angular_core.Signal; private readonly _actionWatcher; readonly displayTime: _angular_core.Signal; componentEvents: { label: string; value: string; }[]; start(): void; stop(): void; ngOnDestroy(): void; private readonly _autoStartWatcher; protected getComponentPropertySchema(): PropertySchema[]; protected getComponentDefaultProperties(): EruComponentProperties; protected getComponentDefaultStyles(): StyleProperties; static ɵfac: _angular_core.ɵɵFactoryDeclaration; static ɵcmp: _angular_core.ɵɵComponentDeclaration; } declare class FlexContainerComponent extends BaseComponent { private componentRegistry; /** * Signal-backed input for nesting level. * Using @Input with a setter to avoid InputSignal brand mismatch issues in library consumers. */ readonly nestingLevel: _angular_core.WritableSignal; set nestingLevelSetter(value: number); readonly selectedComponent: _angular_core.Signal; readonly componentsReady: _angular_core.WritableSignal; private readonly parentEnv; readonly dynamicComponentInjector: EnvironmentInjector; protected isChildHovered: _angular_core.Signal; /** * Whether `child` should be rendered at all (structural). * * The library's default visibility implementation toggles `display:none` on * an already-mounted element, which means hidden components still mount * their effects (e.g. an inactive board-view grid still fires entity-data * fetches). When this method returns false in design-mode the canvas keeps * showing it; outside design-mode (runtime) we skip rendering entirely so * the component tree (and its data side-effects) is never instantiated. */ isChildRendered(child: EruComponent): boolean; constructor(); onDragOver(event: DragEvent): void; selectComponent(event: MouseEvent, component: EruComponent): void; onComponentDragStart(event: DragEvent, component: EruComponent): void; onComponentDragEnd(event: DragEvent): void; onComponentMouseEnter(event: MouseEvent, component: EruComponent): void; onComponentMouseLeave(event: MouseEvent): void; onCanvasMouseOver(event: MouseEvent, component: EruComponent): void; isContainer(type: string): boolean; hasCustomComponent(type: string): boolean; getComponentClass(type: string): any; getZIndex(level: number): number; protected getComponentDefaultProperties(): any; getFlexStyles(): any; getChildFlexStyles(child: EruComponent): any; getCombinedFlexStyles(): any; protected getComponentPropertySchema(): PropertySchema[]; static ɵfac: _angular_core.ɵɵFactoryDeclaration; static ɵcmp: _angular_core.ɵɵComponentDeclaration; } declare class GridContainerComponent extends BaseComponent { private componentRegistry; private destroyRef; /** * Signal-backed input for nesting level. * Using @Input with a setter to avoid InputSignal brand mismatch issues in library consumers. */ readonly nestingLevel: _angular_core.WritableSignal; set nestingLevelSetter(value: number); readonly selectedComponent: _angular_core.Signal; readonly componentsReady: _angular_core.WritableSignal; private readonly parentEnv; readonly dynamicComponentInjector: EnvironmentInjector; protected isChildHovered: _angular_core.Signal; private readonly observedWidth; private resizeObserver; readonly collapseBelowWidth: _angular_core.Signal; readonly isCollapsed: _angular_core.Signal; constructor(); private teardownResizeObserver; onDragOver(event: DragEvent): void; selectComponent(event: MouseEvent, component: EruComponent): void; onGridContainerClick(event: MouseEvent): void; onComponentDragStart(event: DragEvent, component: EruComponent): void; onComponentDragEnd(event: DragEvent): void; onComponentMouseEnter(event: MouseEvent, component: EruComponent): void; onComponentMouseLeave(event: MouseEvent): void; onCanvasMouseOver(event: MouseEvent, component: EruComponent): void; isContainer(type: string): boolean; hasCustomComponent(type: string): boolean; getComponentClass(type: string): any; getZIndex(level: number): number; protected getComponentDefaultProperties(): any; getGridStyles(): any; getChildGridStyles(child: EruComponent): any; readonly combinedGridStyles: _angular_core.Signal; protected getComponentPropertySchema(): PropertySchema[]; static ɵfac: _angular_core.ɵɵFactoryDeclaration; static ɵcmp: _angular_core.ɵɵComponentDeclaration; } /** * A card owns its chrome — surface, header, image, actions row — and nothing * else. Layout belongs to the flex containers it holds in up to three slots * (header, body, actions), the same arrangement tabs use for tab content: the * slot carries the canvas contract (`data-container-id`, `data-component-id`, * `data-nesting-level`, drag and click handlers) so components dropped inside * are droppable, selectable and outlined like anywhere else on the canvas. * * The actions row is a slot rather than a built-in button because a real Button * component already carries the whole event system; a hardcoded button could * never be given an action. */ declare class CardComponent extends BaseComponent { private componentRegistry; private readonly parentEnv; readonly nestingLevel: _angular_core.InputSignal; readonly selectedComponent: _angular_core.Signal; readonly componentsReady: _angular_core.WritableSignal; readonly dynamicComponentInjector: EnvironmentInjector; readonly appearance: _angular_core.Signal<"filled" | "outlined" | "raised">; readonly elevationShadow: _angular_core.Signal; readonly headerType: _angular_core.Signal<"none" | "text" | "custom">; readonly showActions: _angular_core.Signal; readonly actionsAlign: _angular_core.Signal<"start" | "end">; readonly imageUrl: _angular_core.Signal; readonly imagePosition: _angular_core.Signal<"top" | "bottom">; readonly avatarUrl: _angular_core.Signal; readonly titleStyles: _angular_core.Signal>; readonly subtitleStyles: _angular_core.Signal>; readonly headerSlot: _angular_core.Signal; readonly bodySlot: _angular_core.Signal; readonly actionsSlot: _angular_core.Signal; /** The `content` text is a fallback for a card whose body was never filled. */ readonly fallbackContent: _angular_core.Signal; private readonly placeholders; constructor(); getSlotComponentClass(slot: EruComponent): any; getComponentClass(type: string): any; getZIndex(level: number): number; isSlotHovered(slot: EruComponent): boolean; onSlotMouseEnter(event: MouseEvent, slot: EruComponent): void; onSlotMouseLeave(event: MouseEvent): void; onSlotDragStart(event: DragEvent): void; selectSlot(event: MouseEvent, slot: EruComponent): void; private partStyles; private slotId; private slotComponent; /** * Keeps the card's children as its slots, in render order. Children saved * before slots existed — or dropped straight onto the card's chrome — are * moved into the body, which is where they were meant to land. A slot that * is switched off is kept, never deleted, so its content survives the toggle. */ private reconcileSlots; private createSlotContainer; protected getComponentPropertySchema(): PropertySchema[]; protected getComponentStyleSchema(): StyleSchema[]; protected getComponentDefaultProperties(): EruComponentProperties; protected getComponentDefaultStyles(): StyleProperties; static ɵfac: _angular_core.ɵɵFactoryDeclaration; static ɵcmp: _angular_core.ɵɵComponentDeclaration; } declare class DividerComponent extends BaseComponent { protected getExcludedProperties(): string[]; protected getComponentPropertySchema(): PropertySchema[]; protected getComponentDefaultProperties(): EruComponentProperties; protected getComponentDefaultStyles(): StyleProperties; static ɵfac: _angular_core.ɵɵFactoryDeclaration; static ɵcmp: _angular_core.ɵɵComponentDeclaration; } declare class ExpansionPanelComponent extends BaseComponent implements OnInit { private componentRegistry; readonly nestingLevel: _angular_core.InputSignal; readonly selectedComponent: _angular_core.Signal; private readonly parentEnv; readonly dynamicComponentInjector: EnvironmentInjector; private findChild; getHeaderContainer(): EruComponent | null; getContentContainer(): EruComponent | null; getComponentClass(type: string): any; isContainer(type: string): boolean; selectComponent(event: MouseEvent, component: EruComponent): void; protected getComponentPropertySchema(): PropertySchema[]; protected getComponentDefaultProperties(): EruComponentProperties; protected getComponentDefaultStyles(): StyleProperties; ngOnInit(): void; static ɵfac: _angular_core.ɵɵFactoryDeclaration; static ɵcmp: _angular_core.ɵɵComponentDeclaration; } declare class ListComponent extends BaseComponent { readonly listItems: _angular_core.Signal; protected getComponentPropertySchema(): PropertySchema[]; protected getComponentDefaultProperties(): EruComponentProperties; protected getComponentDefaultStyles(): StyleProperties; static ɵfac: _angular_core.ɵɵFactoryDeclaration; static ɵcmp: _angular_core.ɵɵComponentDeclaration; } declare class StepperComponent extends BaseComponent { private stepper; private componentRegistry; private _selectedStepIndex; readonly nestingLevel: _angular_core.InputSignal; readonly selectedComponent: _angular_core.Signal; readonly componentsReady: _angular_core.WritableSignal; readonly selectedStepIndex: _angular_core.Signal; readonly steps: _angular_core.Signal; constructor(); protected getComponentPropertySchema(): PropertySchema[]; protected getComponentDefaultProperties(): EruComponentProperties; protected getComponentDefaultStyles(): StyleProperties; onStepperClick(event: MouseEvent): void; onComponentMouseEnter(event: MouseEvent, component: EruComponent): void; onComponentMouseLeave(event: MouseEvent): void; onCanvasMouseOver(event: MouseEvent, component: EruComponent): void; isContainer(type: string): boolean; hasCustomComponent(type: string): boolean; getComponentClass(type: string): any; getZIndex(level: number): number; isComponentHovered(): (componentId: string) => boolean; onComponentDragStart(event: DragEvent, component: EruComponent): void; selectComponent(event: MouseEvent, component: EruComponent): void; onStepContainerClick(event: MouseEvent, stepContainerComponent: EruComponent): void; hasChildren(): boolean; getStepContainerComponent(stepIndex: number): any; private initializeStepContainers; getStepContainerComponentData(stepIndex: number): EruComponent; private findStepIndexForComponent; private findComponentInChildren; readonly shouldValidate: _angular_core.Signal; isStepCompleted(stepIndex: number): boolean; private markCurrentStepControlsTouched; onNextStep(): void; private collectComponentIds; onStepSelectionChange(event: any): void; static ɵfac: _angular_core.ɵɵFactoryDeclaration; static ɵcmp: _angular_core.ɵɵComponentDeclaration; } declare class SidebarStepperComponent extends BaseComponent { private componentRegistry; readonly nestingLevel: _angular_core.InputSignal; readonly selectedComponent: _angular_core.Signal; private readonly parentEnv; readonly dynamicComponentInjector: EnvironmentInjector; private _activeStepIndex; readonly activeStepIndex: _angular_core.Signal; private _visitedSteps; readonly steps: _angular_core.Signal; readonly stepSubtitles: _angular_core.Signal; readonly sidebarWidth: _angular_core.Signal; readonly activeColor: _angular_core.Signal; readonly completeColor: _angular_core.Signal; readonly showProgress: _angular_core.Signal; readonly progressPercent: _angular_core.Signal; readonly shouldValidate: _angular_core.Signal; componentEvents: { label: string; value: string; }[]; constructor(); private runCompletion; setActiveStep(index: number): void; isStepCompleted(stepIndex: number): boolean; private markCurrentStepControlsTouched; private collectComponentIds; isStepVisited(index: number): boolean; getStepState(index: number): 'active' | 'complete' | 'error' | 'pending'; getActiveStepContainer(): EruComponent | null; getAllStepContainers(): EruComponent[]; getComponentClass(type: string): any; isContainer(type: string): boolean; selectComponent(event: MouseEvent, component: EruComponent): void; protected getComponentPropertySchema(): PropertySchema[]; protected getComponentDefaultProperties(): EruComponentProperties; protected getComponentDefaultStyles(): StyleProperties; static ɵfac: _angular_core.ɵɵFactoryDeclaration; static ɵcmp: _angular_core.ɵɵComponentDeclaration; } declare class TreeComponent extends BaseComponent { treeControl: NestedTreeControl; dataSource: MatTreeNestedDataSource; readonly treeData: _angular_core.Signal; hasChild: (_: number, node: any) => boolean; protected getComponentPropertySchema(): PropertySchema[]; protected getComponentDefaultProperties(): EruComponentProperties; protected getComponentDefaultStyles(): StyleProperties; static ɵfac: _angular_core.ɵɵFactoryDeclaration; static ɵcmp: _angular_core.ɵɵComponentDeclaration; } declare class GridListComponent extends BaseComponent { readonly gridItems: _angular_core.Signal; protected getComponentPropertySchema(): PropertySchema[]; protected getComponentDefaultProperties(): EruComponentProperties; protected getComponentDefaultStyles(): StyleProperties; static ɵfac: _angular_core.ɵɵFactoryDeclaration; static ɵcmp: _angular_core.ɵɵComponentDeclaration; } declare class RadioComponent extends BaseComponent { readonly radioOptions: _angular_core.Signal<{ value: any; label: string; }[]>; protected getComponentPropertySchema(): PropertySchema[]; protected getComponentDefaultProperties(): EruComponentProperties; protected getComponentDefaultStyles(): StyleProperties; static ɵfac: _angular_core.ɵɵFactoryDeclaration; static ɵcmp: _angular_core.ɵɵComponentDeclaration; } declare class SliderComponent extends BaseComponent { private resolveByPath; readonly resolvedValue: _angular_core.Signal; readonly sliderValue: _angular_core.Signal; protected getComponentPropertySchema(): PropertySchema[]; protected getComponentDefaultProperties(): EruComponentProperties; protected getComponentDefaultStyles(): StyleProperties; static ɵfac: _angular_core.ɵɵFactoryDeclaration; static ɵcmp: _angular_core.ɵɵComponentDeclaration; } declare class SlideToggleComponent extends BaseComponent { targetElementSelector: string; private _mode; readonly currentMode: _angular_core.Signal<"view" | "edit">; /** * Which side of the switch the label sits on, in Material's own terms — * `after` is its default and puts the label on the right, which is why the * label used to have nowhere else to go. */ readonly labelPosition: _angular_core.Signal<"before" | "after">; constructor(); onDisplayDoubleClick(): void; onToggleChange(checked: boolean): void; private resetToDefaultMode; protected supportsValueSource(): boolean; private resolveByPath; private toBool; private resolveToggleState; private toggleValueFor; /** * The toggle's own Checked property, read WITHOUT the schema default. * * `getProperty('checked')` substitutes the default `false` whenever nothing is * stored, so "never configured" and "configured off" were the same value — * which made every fallback below unreachable and is why a configured Default * Value never showed. Reading it raw keeps the two apart. */ private readonly explicitChecked; readonly resolvedChecked: _angular_core.Signal; /** * The field's Default Value (or its data-model default), as a boolean. * * Base's seeding effect already writes this into page data — but only for a * field with a Component Name, so a toggle without one has no page-data slot * to be seeded and has to resolve its default here. Resolving it directly also * removes the one-tick flash of "off" while that effect runs. */ private defaultChecked; protected onValueChange(checked: boolean): void; protected getComponentPropertySchema(): PropertySchema[]; protected getComponentDefaultProperties(): EruComponentProperties; protected getComponentDefaultStyles(): StyleProperties; static ɵfac: _angular_core.ɵɵFactoryDeclaration; static ɵcmp: _angular_core.ɵɵComponentDeclaration; } declare class AutocompleteComponent extends BaseComponent { readonly autocompleteOptions: _angular_core.Signal; protected getComponentPropertySchema(): PropertySchema[]; protected getComponentDefaultProperties(): EruComponentProperties; protected getComponentDefaultStyles(): StyleProperties; static ɵfac: _angular_core.ɵɵFactoryDeclaration; static ɵcmp: _angular_core.ɵɵComponentDeclaration; } interface Country { code: string; dialCode: string; flag: string; name: string; } declare class PhoneComponent extends BaseComponent { phoneContainer: ElementRef; selectedCountry: _angular_core.WritableSignal; phoneNumber: _angular_core.WritableSignal; countrySearchText: _angular_core.WritableSignal; phoneNumberControl: FormControl; private readonly flagEmojiSupported; private _mode; readonly countries: Country[]; readonly currentMode: _angular_core.Signal<"view" | "edit">; readonly displayValue: _angular_core.Signal; readonly allowedCountries: _angular_core.Signal; readonly filteredCountries: _angular_core.Signal; readonly fullPhoneValue: _angular_core.Signal; displayFlag(country: Country): string; onCountryChange(countryCode: string): void; onCountrySearchChange(searchValue: string): void; onCountrySelectOpenedChange(opened: boolean): void; private updatePhoneValue; constructor(); ngOnInit(): void; onDisplayDoubleClick(): void; onPhoneBlur(): void; onPhoneKeyPress(event: KeyboardEvent): void; onPhoneInput(event: Event): void; onPhonePaste(event: ClipboardEvent): void; private parsePhoneValue; protected getComponentPropertySchema(): PropertySchema[]; static ɵfac: _angular_core.ɵɵFactoryDeclaration; static ɵcmp: _angular_core.ɵɵComponentDeclaration; } /** * Shared shape for the item lists authored in the property panel by the * `menu_items` and `toolbar_items` editors, and read back by the menu and * toolbar components. * * Nesting is carried by `level` rather than by nested arrays: the property * panel is one narrow column, so a flat list with indent / outdent buttons is * what an author can actually edit. `buildItemTree` turns it back into the * tree the templates render. */ interface MenuItemDef { id: string; kind: string; label?: string; icon?: string; value?: string; level?: number; disabled?: boolean; role?: 'menuitem' | 'menuitemradio' | 'menuitemcheckbox'; variant?: string; color?: string; tooltip?: string; row?: number; visible_if?: string; [key: string]: any; } interface MenuItemNode extends MenuItemDef { children: MenuItemNode[]; } interface ToolbarRow { row: number; items: MenuItemNode[]; } /** * A `mat-toolbar` whose contents are authored from the property panel: titles, * text, buttons, icon buttons, menus, spacers and dividers, in the order the * `toolbar_items` editor lists them. Indenting a row under a Menu item makes * it an entry of that menu. * * Every control emits the one `toolbar_action` event, carrying the item that * was pressed — the same shape `nav_menu` uses for its selection — so an * author wires one handler and switches on the value. */ declare class ToolbarComponent extends BaseComponent { componentEvents: { label: string; value: string; }[]; /** * Top-level items with their menu entries attached. Anything an author * indented rows under opens a menu, whatever kind they picked for it — the * alternative is entries that exist in the editor and render nowhere. */ readonly items: _angular_core.Signal; /** * Items grouped into `mat-toolbar-row`s. Material throws when direct content * and rows are mixed, so a multi-row toolbar puts EVERY item in a row — * including the ones an author left without a row number, which land on * row 1. */ readonly rows: _angular_core.Signal; readonly colorClass: _angular_core.Signal; itemColorClass(item: MenuItemNode): string; itemAppearance(item: MenuItemNode): 'text' | 'filled' | 'elevated' | 'outlined' | 'tonal'; /** See `menu`: `visible_if` is evaluated fail-closed. */ private visibleItems; onItemAction(item: MenuItemNode): void; protected getComponentPropertySchema(): PropertySchema[]; protected getComponentDefaultProperties(): EruComponentProperties; protected getComponentDefaultStyles(): StyleProperties; static ɵfac: _angular_core.ɵɵFactoryDeclaration; static ɵcmp: _angular_core.ɵɵComponentDeclaration; } /** * A `mat-menu` and the control that opens it, both authored from the property * panel: items come from the `menu_items` editor, where indenting a row makes * it a submenu entry. * * On the canvas the overlay is left shut and the items are listed inline * instead — an author has to see what they are editing, and an overlay would * cover the component being selected. */ declare class MenuComponent extends BaseComponent { componentEvents: { label: string; value: string; }[]; readonly openMode: _angular_core.Signal; readonly triggerAppearance: _angular_core.Signal<"text" | "elevated" | "filled" | "outlined" | "tonal">; /** Material's `color` input is M2-only; the studio theme is M3, so the * choice is carried by a class that overrides the button's M3 tokens. */ readonly triggerColorClass: _angular_core.Signal; readonly items: _angular_core.Signal; /** Flattened items behind the canvas preview, indent included. */ readonly previewItems: _angular_core.Signal; /** * `visible_if` is evaluated FAIL-CLOSED, as in `nav_menu`: an expression that * throws or cannot be resolved hides the item. The shared evaluator resolves * errors to `true` so a mis-typed layout condition does not blank a page, but * an item an author meant to hide must not appear because its condition was * mis-typed. */ private visibleItems; onItemSelect(item: MenuItemNode): void; onMenuOpened(): void; onMenuClosed(): void; protected getComponentPropertySchema(): PropertySchema[]; protected getComponentDefaultProperties(): EruComponentProperties; protected getComponentDefaultStyles(): StyleProperties; static ɵfac: _angular_core.ɵɵFactoryDeclaration; static ɵcmp: _angular_core.ɵɵComponentDeclaration; } declare class SidenavComponent extends BaseComponent { protected getComponentPropertySchema(): PropertySchema[]; protected getComponentDefaultProperties(): EruComponentProperties; protected getComponentDefaultStyles(): StyleProperties; static ɵfac: _angular_core.ɵɵFactoryDeclaration; static ɵcmp: _angular_core.ɵɵComponentDeclaration; } declare class TabsComponent extends BaseComponent { private componentRegistry; readonly nestingLevel: _angular_core.InputSignal; readonly selectedComponent: _angular_core.Signal; readonly componentsReady: _angular_core.WritableSignal; readonly tabs: _angular_core.Signal; private readonly _activeTabIndex; readonly activeTabIndex: _angular_core.Signal; readonly headersHidden: _angular_core.Signal; readonly alignTabs: _angular_core.Signal; readonly stretchTabs: _angular_core.Signal; readonly headerPosition: _angular_core.Signal<"above" | "below">; readonly dynamicHeight: _angular_core.Signal; readonly fitInkBarToContent: _angular_core.Signal; readonly disableRipple: _angular_core.Signal; readonly animationDuration: _angular_core.Signal; /** * Tab containers in tab order. Order in `children` — not the number in the * id — is what maps a container to a tab, so renaming, reordering or * deleting a tab never has to renumber ids and can never point a tab at * another tab's content. */ readonly tabContainers: _angular_core.Signal; private readonly placeholders; constructor(); protected getComponentPropertySchema(): PropertySchema[]; protected getComponentDefaultProperties(): EruComponentProperties; protected getComponentDefaultStyles(): StyleProperties; /** * The tab an author named, as an index. A label is matched first — that is * what an author reads off the Tabs list — and a number is taken as a * position, so either can be written into the action (or produced by its * expression) without a second setting to say which was meant. */ private resolveTabIndex; onSelectedIndexChange(index: number): void; onTabsClick(event: MouseEvent): void; onComponentMouseEnter(event: MouseEvent, component: EruComponent): void; onComponentMouseLeave(event: MouseEvent): void; onCanvasMouseOver(event: MouseEvent, component: EruComponent): void; isContainer(type: string): boolean; hasCustomComponent(type: string): boolean; getComponentClass(type: string): any; getZIndex(level: number): number; isComponentHovered(): (componentId: string) => boolean; onComponentDragStart(event: DragEvent, component: EruComponent): void; selectComponent(event: MouseEvent, component: EruComponent): void; hasChildren(): boolean; getTabContainerComponent(tabIndex: number): any; getTabContainerComponentData(tabIndex: number): EruComponent; private tabIdPrefix; private parseTabs; private tabLabelOf; private withTabLabel; private createTabContainer; /** One past the highest id already in use, so a new tab can never take an id a live tab still holds. */ private nextTabSequence; /** * Line the tab titles up with the containers that hold their content. * * Containers are matched to titles by title first, then by position for * whatever is left over, so an edit only ever touches the tab it was aimed * at: reordering re-sorts containers, renaming keeps the container in place, * removing a tab drops only that tab's container, and adding one appends an * empty container. The previous version rebuilt every container whenever the * count changed, which emptied every tab on the page. */ private reconcileTabContainers; static ɵfac: _angular_core.ɵɵFactoryDeclaration; static ɵcmp: _angular_core.ɵɵComponentDeclaration; } interface NavItem { id: string; label: string; description?: string; icon?: string; page?: string; group?: string; badge?: string | number; /** * Optional expression for shaping navigation — hiding an item a user COULD * reach. NOT a permission: page access is what decides whether the page can * be opened, and the host has already dropped items whose target page is not * in the user's visible set before they reach this component. * * Evaluated fail-CLOSED, unlike every other condition in the library: a * malformed layout condition should leave a component visible, but a * malformed one here would reveal navigation an author meant to hide. */ visible_if?: string; } interface NavGroup { label: string; items: NavItem[]; } /** * Sidebar / horizontal nav for shell-app pages. Renders grouped, iconed, * badged items. Clicking an item writes the item's `page` value to the URL * via `EruAppRouteService` under `route_param_name` (default `view`). * * Pair with `nav_outlet` (which reads the same param) to swap the active * page on the right. The two components are decoupled — they share state * only via the URL. */ declare class NavMenuComponent extends BaseComponent { componentEvents: { label: string; value: string; }[]; readonly collapsed: _angular_core.WritableSignal; ngOnInit(): void; constructor(); private lastSelectedKey; /** * Items fetched from a process menu, or null when none has been requested. * * The host answers `ds_menu` with the menu ALREADY filtered to the pages this * user may open — the library has no business knowing about roles, and the * page set lives in the host. What is left for this component is the * presentation half: `visible_if` and dropping groups it empties. */ private readonly menuItems; private readonly menuRequestId; private lastMenuId; /** Raw items: the referenced menu when there is one, else the inline JSON. */ private readonly rawItems; /** * Items after `visible_if`, which is evaluated FAIL-CLOSED: an expression * that throws, or references something not yet resolvable, hides the item. * * The shared evaluator resolves errors to `true` because a mis-typed layout * condition should not blank a page. Here the opposite is wanted — a * mis-typed navigation condition must not reveal an item — so the result is * only trusted when the evaluator returns a real boolean. */ readonly parsedItems: _angular_core.Signal; /** Items grouped by `group` field. Items with no group land in a single * '' bucket rendered first. */ readonly groups: _angular_core.Signal; /** * Active item id derived from the URL param, falling back to * `default_item_id` and then to the first visible item — a shell opens on its * first page, so that item is the one shown selected, and On Menu Select has * to fire for it the same as for a click. */ readonly activeItemId: _angular_core.Signal; isItemActive(item: NavItem): boolean; onItemClick(item: NavItem): void; /** * `menu_select` fires on the canvas too. The designer opens in edit mode with * the first item's page already mounted in the outlet, so suppressing it left * the header its handler fills blank while the page beside it was rendered. */ protected suppressEventsInEditMode(eventType: string): boolean; toggleCollapse(): void; protected getComponentPropertySchema(): PropertySchema[]; protected getComponentDefaultProperties(): EruComponentProperties; protected getComponentDefaultStyles(): StyleProperties; static ɵfac: _angular_core.ɵɵFactoryDeclaration; static ɵcmp: _angular_core.ɵɵComponentDeclaration; } /** * Mounts the page identified by a URL query param. Pair with `nav_menu` * (which writes the param). Each mount instantiates a fresh * `RuntimeCanvasComponent` — and therefore a fresh `EruStudioStore` — * so the inner page is independent of the shell. Existing nesting * mechanics (page_ref, isNested, parent walks, page_data) operate * entirely within the inner store, exactly as they would for the * inner page rendered standalone. * * Page-def fetch happens on the OUTER store (this.store from * BaseComponent) because processo's `ds_page` handler only watches * that one. The resolved def is then handed to the inner canvas via * its `pageDef` input. * * If the URL param is empty, falls back to `default_page`. */ declare class NavOutletComponent extends BaseComponent { /** Whether pages mounted here resume their previous state on a return visit. */ readonly retainPageState: _angular_core.Signal; /** Page id to mount, derived from URL query param or default_page fallback. */ readonly mountPageId: _angular_core.Signal; /** Most recent in-flight ds_page request id (keyed in outer store). */ private readonly lastReqId; /** Resolved page def handed to the inner runtime-canvas. */ readonly resolvedPageDef: _angular_core.WritableSignal; /** * Single-element list of the resolved page def (or empty). The template * iterates this with `track def.id` so Angular destroys+recreates the * inner `eru-runtime-canvas` whenever the active page id changes — * giving each page a truly fresh `EruStudioStore` instance and preventing * state pollution from the previously-mounted page. */ readonly resolvedPageDefAsList: _angular_core.Signal; constructor(); protected getComponentPropertySchema(): PropertySchema[]; protected getComponentDefaultProperties(): EruComponentProperties; protected getComponentDefaultStyles(): StyleProperties; static ɵfac: _angular_core.ɵɵFactoryDeclaration; static ɵcmp: _angular_core.ɵɵComponentDeclaration; } declare class GridComponent extends BaseComponent implements OnInit, AfterViewInit { eruGrid: EruGridComponent; gridConfig: GridConfiguration | undefined; private isGridInitialized; private lastSortKey; private _appliedViewMode; private processingRequests; private gridRequestMap; private pendingViewFileGridRequests; private forwardedViewFileGridKeys; private pendingPageListRequests; private forwardedPageListKeys; private pendingSelectOptionRequests; private forwardedSelectOptionKeys; private pendingPeopleGridRequests; private forwardedPeopleGridKeys; private pendingExcelDownloadRequests; private forwardedExcelDownloadIds; private themeObserver?; private allFields; private columnOverrides; /** Last stats request sent, so an unchanged rule set does not refetch. */ private lastColumnStatsKey; private lastTotalsKey; private lastQueryKey; private lastQueryPayload; private static readonly QUERY_DISPATCH_DEBOUNCE_MS; private queryDispatchTimer; private rowsGeneration; /** Generation each in-flight per-group row request was issued under. */ private rowRequestGenerations; /** Empty-selection signature, so the selection effect's first run emits nothing. */ private lastSelectionSignature; private _autoSelectedFirstRow; protected schemaService: ComponentSchemaService; /** Columns to hide for the current viewport: the `hide_columns` multiselect * merged with any manually-typed names from `hide_columns_manual`. The manual * list matters for nested-page grids whose columns aren't known until the * parent query fires, so the dropdown is empty at design time. * * Hiding is additive across breakpoints: the base list is what the author * means by "hidden", and a breakpoint list hides more on top of it. Normal * responsive resolution replaces instead — a breakpoint value wins outright * and base is never read — so one column hidden at, say, `xl` shadowed the * whole base list and every other hidden column came back on a wide screen. * That also contradicted the design pane's Hidden checkbox, which writes to * base precisely because it means "hidden everywhere" (see * routeHiddenToProperty). Unhiding a column for one breakpoint is not * expressible this way, and never was: the multiselect writes a whole array, * so removing a name only ever read as "hide fewer here" by accident. */ private hiddenColumnNames; /** * Layer the user's per-column design overrides (deltas) on top of the * source columns (entity fields / query-inferred). Only attributes the user * actually changed in design mode are stored as overrides, so everything else * keeps flowing from the source. */ /** Per-column overrides, seeded once from the persisted property then accumulated in-memory. */ private getColumnOverrides; /** * Cell editability of a data-model field, in eru-grid's `editable` terms. * * The data model authors this with two separate keys, and which one applies * depends on the field: a derived-from-child field (`dfc`) is governed by its * `editable` toggle ("may this rolled-up value be overridden by hand"), every * other field by the general "Can Edit?" toggle `can_edit` (default true). * eru-grid only knows `editable`, and inheriting that key verbatim read the * rollup toggle for ordinary fields too — where it is stored `false` — which * left every entity-backed column read-only regardless of the data model. */ private entityFieldEditable; /** * Build a grid column from a data-model field. The whole entity field is * passed through — hand-picking a few keys here is what previously left every * configurable datatype (dropdown options, status colours, date formats, * decimal precision, currency symbol, colour bands) rendering with library * defaults inside a studio-hosted grid, no matter how the process was set up. * Only the layout keys the grid owns are withheld; user design overrides are * layered on top afterwards by applyColumnOverrides(). */ private entityFieldToColumn; private applyColumnOverrides; /** * The design pane's Hidden checkbox, redirected to the `hide_columns` * property. * * As a column override it was a one-way door: eru-grid drops an `is_hidden` * column from the header row, and the tune icon that opens the design panel * goes with it, so nothing was left to untick. `hide_columns` is a * multiselect in the properties panel listing every column, so a hide made * there or here can always be undone in the same place. * * Any stale `is_hidden` already sitting in the overrides is explicitly * cleared, or it would keep the column hidden no matter what the property * says. Returns the patch with `is_hidden` removed. */ private routeHiddenToProperty; /** * Write `hide_columns` at base, addressing the component by id. * * Not setProperty(): that goes through updateComponentPropertyAtBreakpoint, * which returns early unless the grid is the *selected* component — and * opening the column design panel from a header does not select it, so the * write was dropped without a word. persistColumnOverrides addresses the * component directly for the same reason. * * Base rather than the active breakpoint because the design pane's checkbox * says "Hidden", not "hidden on this viewport": the `is_hidden` it replaces * applied everywhere, and a base value is what every breakpoint falls back * to. Per-viewport hiding stays the properties panel's job. */ private persistHideColumns; /** * Merge one or more per-column override patches into the in-memory map and * persist the full map at base, by component id (no need for the grid to be * the currently-selected component). */ private persistColumnOverrides; /** * True when the grid's rows can be traced back to a stored record, which is * what any cell edit ultimately has to be written to. * * An entity-backed grid always can — every row is an entity read. The array * sources (query, page field, state) hand over plain objects, and only the * `entity_id_field` property says which of their keys carries the record id. * Nothing infers it: a query that does not select the id has rows that simply * are not addressable, and guessing would write to the wrong record. */ private rowsAreIdentifiable; /** Row key holding the record id, for the array data sources. Blank when unset. */ private entityIdField; /** True when the page has wired up 'On Cell Edit' itself. */ private hasCellEditSubscription; /** * Lock the columns whose save target is unknowable. On the array sources a * column name is a select-list alias that no entity need have, so only a * column carrying a `mapped_entity`/`mapped_field` mapping can be written * back — the rest would open an editor and drop the value on close. * * A configured 'On Cell Edit' subscription lifts this: the author is building * the write themselves off the row, so an alias column is theirs to interpret * and locking it would leave them an event that can never fire. * * Only unmapped columns are touched, which is exactly the set eru-grid's * data-model merge leaves alone — see the `editable` getter for why the * mapped ones are gated grid-wide instead. Applied to what is handed to the * grid and never written back into `allFields`, so the lock can lift again * when a mapping or subscription is added. */ private clampEditableColumns; /** Push the full column set to the grid, with overrides applied, filtered by the current viewport's hidden columns. */ private setGridFields; componentEvents: { label: string; value: string; }[]; /** * Column keys that have at least one 'On Drill' subscription. * * This is what makes a cell a link: eru-grid renders the drill affordance for * any column whose Field carries `enableDrilldown`, and that flag is derived * from here rather than configured separately — a link that drills nowhere, * or a handler no cell can reach, is not a state worth being able to express. * * Case-folded for the same reason custom actions are: the key is typed by * hand against a column name, and a case slip would silently produce a * subscription that never fires. */ private get drillColumnKeys(); /** Stamp `enableDrilldown` onto the columns that have an On Drill handler. */ private applyDrillFlags; /** * A drill click came back from the grid. Every 'On Drill' subscription naming * that column runs, in the order they appear in the page's event list — the * same grouping rule as Custom Action, so one link can drive several actions. * * The payload extends 'On Row Select' with the cell that was clicked: which * column, its value, and in pivot mode the column-dimension path that located * it. `entity_data` is the row, with the pivot coordinate merged in by * eru-grid, so an api/query configured on the event can filter by it. */ private onGridDrilldown; /** * Identity of a custom action, for grouping subscriptions and for matching a * click back to them. Trimmed and case-folded so 'Approve', 'approve ' and * 'APPROVE' are one action rather than three icons that each do part of the * job. Grouping and click-matching must use this same key or a click would * fire a different set than the icon represents. */ private static actionKey; /** * The row actions handed to eru-grid, one per distinct action NAME across the * configured 'Custom Action' subscriptions. * * The action list lives in the events, not in a property of its own: an * action IS its handler, and splitting the icon from what it does would let * the two drift — an icon with no subscription, or a subscription no icon can * reach. * * A subscription carries a single action, so running several things from one * icon means several subscriptions sharing a name. They collapse to ONE icon * here, and onGridActionClick() runs all of them — see there for the ordering * rules. Because the author should not have to restate the presentation on * each one, the icon and visible-condition are merged across the group: the * first non-blank value wins, whichever subscription it was set on. */ private get gridActions(); /** * A row action was clicked in the grid. Every subscription declaring that * action name runs, and nothing else does — see emitEvent's * subscriptionFilter. Several subscriptions sharing a name is how one icon * performs several actions, since a subscription holds only one. * * They run in the order they appear in the page's event list, so an author * orders the steps by ordering the cards. Note that emitEvent stops the whole * run if a subscription with 'Validate Before Action' fails its check — the * remaining steps are skipped rather than half-applied. * * The payload matches 'On Row Select' so a handler written for one works for * the other, with the action name added for handlers that branch on it. */ onGridActionClick(payload: { action: GridAction | null; row: any; mode: 'table' | 'board'; group?: any; }): void; /** * Where an edited cell has to be written back: the entity that owns the field * and the field's name there. * * A column can reach the data model two ways. An entity-backed grid takes its * columns straight from the entity, so the column name IS the field name. A * query-backed grid names its columns after the query's select list, and only * the column's `mapped_entity`/`mapped_field` design mapping says what that * column really is — the column name may be a query alias that no entity has. * * `savable` is what gates the automatic save. A column mapped to an entity * OTHER than the one the grid lists (an account name shown on a deals grid) is * deliberately excluded: the row's entity_id identifies the deal, not the * account, so there is no record to write it to. The event still fires so a * page can handle it with an api/function of its own. */ private cellSaveTarget; /** * The record id of an edited row. Entity-backed rows carry `entity_id`; the * array sources carry it under whatever key `entity_id_field` names, and * nowhere else — eru-grid's own `rowId` falls back to a synthetic * "_" cell id, which would address no record at all. */ private rowEntityId; /** * An editable cell finished an edit session with a changed value. eru-grid * only announces this on its store — it never writes the value back or * persists it — so the wrapper has to pick it up and forward it, or the edit * dies inside the grid. The event carries just the edited field keyed by the * ROW's entity, not the page's, since a grid usually lists something other * than the page it sits on. * * With no subscription configured the grid still saves, via the default * subscription below: an editable cell whose edits silently vanish is never * what the page author meant, and it matches how the same signal is consumed * outside studio pages (processo's data viewer). */ private onCellValueChange; onRowSelect(payload: { row: any; mode: 'table' | 'board'; group?: any; }): void; /** * Checkbox multi-selection, forwarded as `selection_change`. * * Unlike `row_select` (a row *click*, one record) this is the whole tick-box * set, and it is the only route by which selected records become addressable * from a page: an author lands it in page state with `update-state` and every * other requirement — a count, a sum, a button that posts the ids — then falls * out of the state bindings that already exist. * * The payload is namespaced under a single `selection` key so an author's * payload path stays stable and cannot collide with the record fields. */ private onSelectionChange; /** * The descriptor that reproduces exactly the rows this grid is showing — * the same source, payload and sort its own row fetches use, minus the * paging window. A handler acting on "all N matching" sends this instead of * an id list. */ private selectionFilterCriteria; /** * The row key the selection is identified by — the same rule `rowEntityId` * follows for a single edited row, so what the payload claims the id column * is cannot disagree with the ids in `selected_ids`. Entity reads always * answer with `entity_id`; an array source carries one only where the author * named the column in Entity Id Column, and is blank otherwise. */ private selectionIdField; /** * Grid-level editability handed to eru-grid. Beyond the author's own * 'Editable' switch this withdraws editing entirely when an edit could go * nowhere: on the array sources, rows are only addressable once * `entity_id_field` names the key holding the record id, and only then can a * cell edit be saved. A wired-up 'On Cell Edit' subscription is the other way * to qualify — the author is handling the write themselves. * * This is the gate rather than a per-column flag because eru-grid re-merges * `editable` onto every mapped column from its data-model field * (INHERITED_FIELD_KEYS), which would undo a per-column lock. The grid-level * flag sits outside that merge — see the cell template's * `isEditable() && column.editable !== false`. */ get editable(): boolean; get columnResizable(): boolean; get columnReorderable(): boolean; get cellSelection(): boolean; get rowSelection(): boolean; get filtering(): boolean; get showColumnLines(): boolean; get showRowLines(): boolean; get enableRowSubtotals(): boolean; get enableColumnSubtotals(): boolean; get enableColumnGrandTotal(): boolean; get enableGrandTotal(): boolean; get subtotalPosition(): 'before' | 'after'; get subtotalPositionColumn(): 'before' | 'after'; get grandTotalPosition(): 'before' | 'after'; get grandTotalPositionColumn(): 'before' | 'after'; get subtotalLabel(): string; get freezeField(): string; get freezeHeader(): boolean; get freezeGrandTotal(): boolean; get replaceZeroValue(): string; get gridHeight(): number; /** Rows fetched per lazy page. Undefined lets eru-grid use its own default. */ get pageSize(): number | undefined; get allowSelection(): boolean; get actionColumn(): boolean; get actionPosition(): 'before' | 'after'; get actionDisplayType(): 'icons' | 'icons_outlined' | 'kebab'; get sortable(): boolean; get sortBar(): boolean; get groupBar(): boolean; get headerRowHeight(): number; get dataRowHeight(): number; get cursorOnHover(): string; get enableExcelDownload(): boolean; get showExcelDownloadIcon(): boolean; get showRefreshIcon(): boolean; get responseKey(): string; /** eru-grid expects fill colours as an array; the panel edits a single colour. */ private colorList; /** A colour property, resolved to a literal hex value (or undefined). */ private colorOrUndefined; /** * Excel understands literal colours only, so a design token picked in the * panel (`var(--studio-primary)`) has to be resolved before it leaves the * studio. The token chain is resolved by the browser: the value is applied to * a hidden probe inside this grid's element (so nested theme scopes and the * active light/dark branch apply), and the used colour is read back. Returns * '' when it cannot be resolved, so the key is dropped rather than exported * as some unrelated colour. */ private resolveColor; /** * Paint the colour on a 1×1 canvas and read the pixel back — that leaves the * oklch/lab → sRGB conversion (and any alpha compositing) to the browser, * which plain string parsing cannot do. Fully transparent yields ''. */ private toHexColor; private static _colorCtx; private static colorContext; private numberOrUndefined; private textOrUndefined; /** Excel header/data style config forwarded to eru-grid, only when the excel * download icon is enabled. Omitted keys keep eru-grid's own fallbacks. */ private buildExcelConfig; /** * Adapt the ds_people user directory into the option records eru-grid's people * cell reads: it builds each label as `fn ln` and each value from `id`. The * user directory carries one `user_name`, so it is split on the first space. */ /** Append a page of options to what the column already holds, without * repeating a value that is already there. */ private mergeSelectOptions; private toGridPeopleOptions; /** * Wrap an eru-grid ExcelDownloadRequest into the payload the host app needs to * run the export itself. The grid supplies `columns` / `excel_styles`; this * adds the data-source context so the host can pick the right route — the * saved query's `/excel` variant, or the entity fetch with `excel: true`. * No row window (qlimit/qskip) is sent: the export covers the full result. */ private buildExcelDownloadPayload; get viewMode(): 'table' | 'board' | 'pivot'; get preset(): GridPreset; get boardCardHeight(): number; get boardCardGap(): number; get boardCardPadding(): number; get cardPageId(): string; get boardCardBg(): string; get boardCardHoverBg(): string; get boardCardSelectedBg(): string; get boardCardSelectedOutline(): string; /** * Order the groups themselves are returned in, as an array so more than one * field can drive it later; today the panel offers a single field. Empty array * when unset, so the payload key is always present and the API never has to * distinguish "not sent" from "no ordering". */ get groupOrderBy(): string[]; get boardCountTextColor(): string; get boardCountBg(): string; get boardColumnHeader(): boolean; get boardColumnMinWidth(): number | undefined; /** Cap on board column width. Undefined (property left blank) = stretch to fill. */ get boardColumnMaxWidth(): number | undefined; /** Cap on board column height. 0 = no cap (columns end after their cards); * undefined (property left blank) = eru-grid's own default. */ get boardColumnHeight(): number | undefined; /** Board columns wrap onto the next row (default) or stay on one scrolling row. */ get boardWrapColumns(): boolean; private parseFieldList; get pivotRows(): string[]; get pivotCols(): string[]; get pivotAggregations(): Field[]; /** * Returns the board card template only when board mode + a card page are configured. * Cast to `any` to sidestep cross-bundle TemplateRef identity mismatches between * eru-grid's compiled types and the consumer's Angular runtime types. */ resolvedBoardTemplate(tpl: any): any; /** Same structural-type escape hatch as resolvedBoardTemplate, for the * per-cell page renderer. Always supplied: eru-grid only invokes it for a * column whose datatype is 'page'. */ resolvedCellTemplate(tpl: any): any; private static readonly GRID_TOKEN_PROPS; /** Build the grid `tokens` map: static alignment tokens plus any design-token * colours the user set in the panel (token or custom). * * Every colour key is always emitted, blank when unset. eru-grid paints * these as inline CSS vars on its host and only clears the keys it is * handed, so omitting an unset key left the previous colour painted (and * `!important`, so it beat the preset): clearing a token, or switching the * preset away from Custom, showed no change until the page was reloaded. */ private buildGridTokens; private buildGridConfig; ngAfterViewInit(): void; constructor(); /** * Persist a design-panel edit onto the matching pivot aggregation. * * Returns false when the patched name is not an aggregation, so the caller * falls through to the normal per-column override path. */ private persistAggregationMeta; /** * Drop every row on screen and the bookkeeping for the pages still in flight, * and start a new generation. * * Clearing `processingRequests` is what lets the new group list ask for its * first page again, but it does NOT stop the previous list's pages from * coming back — the requests are already out. Those answers name a groupId * that usually still exists, so they used to be appended to the rows of the * load that replaced them. The generation counter is what tells them apart. */ private resetLoadedRows; /** * Send a query/function fetch after a short quiet period, replacing one that * has not gone out yet. * * The caller has already banked the fingerprint, so an unchanged payload * never gets here; what this collapses is a run of DIFFERENT payloads * arriving within milliseconds of each other, where only the last one is the * state the user is actually looking at. */ private scheduleQueryDispatch; /** True when a row page belongs to a load that has since been replaced. */ private isStaleRowResponse; /** * Re-page every group against the same group list. The counters are reset so * eru-grid asks for page 0 again; the group field is unchanged, so setGroups() * leaves sortColumns alone and no executegroup call is made — group ordering * is `group_order_by`'s business, not a header sort's. */ private reloadRowsForCurrentGroups; private refreshGrid; /** * Extract the rows array from a query response. When a 'Result Path' property * (dot path, e.g. "0.pouches") is set, the rows are read from that location; * otherwise falls back to the response itself / its entity_data. */ /** * Map an executegroup result into the grid's RowGroup[] — mirrors the entity * grouping shape so the lazy per-group row fetch reuses the same plumbing. * executegroup returns the group value under the group-by field name (e.g. * account_type) and the count under `cnt` (or the count aggregation's alias), * NOT gb_field/count — so read those out per row. The group value becomes the * RowGroup id and is sent as the `qfilter` { [groupBy]: value } row query. */ private readBindingRows; /** Coerce a bound value into an array of row objects: accepts an array, a * JSON-string array, or a single object (wrapped). Anything else → []. */ private coerceToRowArray; /** Render a ready-made array of row objects as a single whole-result group * (used by the page_field / state data sources). Columns auto-derive from the * first row, exactly like the query whole-result path. */ private renderRowsDirectly; private buildQueryGroups; /** * The column holding each group's row count in an executegroup result. When * no aggregations are passed the backend returns `cnt`; otherwise it returns * the alias of the aggregation whose func is `count`. */ private groupCountKey; /** Read the total from an executegroup count row: the count-key column, else * cnt/count, else the first numeric value. */ private countFromRow; /** * Column datatypes the table-mode total rows can aggregate. Everything else * (text, dates, statuses) leaves its total cell blank, which is what the * subtotal/grand-total templates render for a missing key. */ private static readonly TOTALLABLE_DATATYPES; /** * Whether the server has to compute total rows for this grid. Table mode only * — a pivot aggregates client-side in eru-grid, and a board has no total row. * The two row flags, because that is what a table draws: a subtotal row under * each group and one grand-total row. The `...Column` pair beside them adds a * total COLUMN and only means anything in pivot. */ private get totalsEnabled(); /** * The aggregations the executegroup call needs so the total rows have values. * * Every visible number/currency column, aliased to its own column name — the * subtotal/grand-total templates look each cell up by column name, so no * mapping table is needed on the way back. `sum` unless the author picked a * different function for that column in the design panel; that one choice * feeds both total rows, since a column summed in one and averaged in the * other reads as two different measures. * * A `count` is always asked for alongside, aliased `cnt`: the group list's * row counts drive pagination, and once aggregations are passed the backend * returns the count under an alias rather than its default `cnt` column. */ private buildTotalsAggregations; /** * What actually goes out on the executegroup call. A hand-written * `query_aggregations` wins verbatim — it is the escape hatch for aggregates * the design panel cannot express, and pages that already rely on it must not * change behaviour. */ private groupAggregations; /** * Fetch the total rows when the group call could not carry the aggregations. * * A query grid types its columns from the first page of results, so on a cold * load the executegroup call goes out before the grid knows which columns are * numeric. Once it does, the totals are fetched on their own — the grand total * from an ungrouped pass, the subtotals from a grouped one that only patches * `RowGroup.subtotal`. Keyed on the aggregation list so this fires once per * change, not on every page of rows. */ private syncQueryTotals; /** * Settle the grand-total row after a group-list response. * * With no group-by the list is a single row covering everything, so it is * already the grand total — no second call. With a group-by it is one row per * group, and the grand total needs the same query aggregated over all rows, * which is one more executegroup with an empty group_by. That extra trip only * happens when the grand-total row is actually switched on. */ private applyQueryGrandTotal; /** * Settle the grand-total row after an entity group response. * * processo maps the entity group call into RowGroup[] itself, subtotals * included, so there is nothing to re-map here. Ungrouped, the single group * covers every row and its subtotal is the grand total. Grouped, the grand * total would need the same second pass the query source makes — but the * entity group call goes through one shared request/response signal in * processo, which cannot carry two in flight, so it is left unset rather than * shown wrong. */ private applyEntityGrandTotal; /** * Read the aggregate columns out of one executegroup row. Keys are the column * names the aggregations were aliased to, so the result drops straight into * `RowGroup.subtotal` / `setRowGrandTotal()`. `cnt` is left out — it is the * row count, not a measure, and would print in the first numeric column. */ /** * Ask the server for the aggregates the grid's formatting rules need. * * A separate call from the totals one on purpose: `buildTotalsAggregations` * builds its list from column config and its response drives the visible * grand-total row, so folding min/max/percentile into it would surface * measures in that row the author never configured. Ungrouped, because these * describe the whole column. */ private requestColumnStats; /** Map the single aggregate row onto the alias->value map the grid reads. */ private applyColumnStats; private totalsFromRow; /** Parse the `query_aggregations` JSON property into an array (or undefined). */ private parseQueryAggregations; private extractQueryRows; /** * Build the query payload from the static JSON property merged with the * selected state/page payload fields (state:* / page:* tokens resolved live). */ private buildQueryPayload; /** * eru-grid's multi-column sort, in the order the user built it: `["pn", "-da"]` * — a leading `-` means descending. The grid keeps this as UI state only and * never reorders rows itself, so the query has to apply it; it rides the * execute-query call as `qsort`. * * Distinct from the `group_order_by` property, which orders the group list on * the executegroup call and is authored in the panel. Row ordering and group * ordering are separate settings and never share a payload key. */ private gridSortColumns; /** * Resolve `state:` / `page:` payload-field tokens into a * {key: value} object using the logic evaluator. Mirrors the page-ref pattern. */ private resolveApiPayload; protected getComponentPropertySchema(): PropertySchema[]; ngOnInit(): void; ngOnDestroy(): void; static ɵfac: _angular_core.ɵɵFactoryDeclaration; static ɵcmp: _angular_core.ɵɵComponentDeclaration; } /** * Turning a query's tabular result into chart marks. * * The charts used to take one column for the label and one for the value and * plot the rows as they arrived, which only works when the query has already * grouped. Real result sets are wide and long — one row per transaction, many * columns — so the chart has to do the grouping itself. * * The vocabulary is the grid's pivot vocabulary on purpose (see the * `pivot_rows` / `pivot_cols` / `pivot_aggregations` properties on * `grid.component.ts`): a dimension is what a grid pivot puts down the left, a * split is what it spreads across the top, and a measure is one of its values * with its aggregation function. A page author who has configured a pivot grid * already knows how to configure a chart. */ type ChartAggregation = 'sum' | 'count' | 'avg' | 'min' | 'max' | 'first' | 'none'; interface ChartDimension { name: string; label?: string; /** Column a click on this dimension's marks drills into. Blank: no drill. */ drill_into?: string; /** Label for the drilled-to level; falls back to that column's own label. */ drill_label?: string; /** Top-N cap for the drilled-to level. */ drill_top_n?: number; /** Query to run for the drilled-to level. Query-sourced drills only. */ drill_query?: string; /** * Payload key the clicked categories are sent under when the drill re-queries. * Absent, this dimension's own column name is used. */ drill_filter_field?: string; /** Entity holding the colour-bearing field. Blank uses the page's entity. */ color_entity?: string; /** * Entity field whose option colours paint this dimension's marks. Blank uses * the dimension's own column, which is the usual case — a `ds` dimension takes * the `ds` field's status colours with nothing to configure. */ color_field?: string; /** * Per-value colour overrides for THIS dimension, as `Value=#hex` pairs or * `{value, color}` rows. Per dimension because every dimension is a different * column: the values under deal status share nothing with those under risk. */ value_colors?: any; /** * Highest N categories to plot for THIS dimension, rest clubbed into 'Others'. * Per dimension because cardinality is a property of the column: deal status * may have twenty values worth capping while deal type has three. Absent plots * every category. */ top_n?: number; } interface ChartMeasure { name: string; label?: string; aggregation?: ChartAggregation; /** Entity holding the field whose number format this measure follows. */ format_entity?: string; /** * Field whose `decimal` / `seperator` / `dynamic_number` / `symbol` this * measure formats by. Blank uses the measure's own column, so an amount column * already formatted as currency in the model needs nothing configured. */ format_field?: string; /** Inline overrides for when the model carries no format. */ symbol?: string; decimal?: any; seperator?: string; dynamic_number?: boolean; display_number_as?: 'lacs' | 'mn'; } /** The rows behind one category, kept so a drill click can hand a record back. */ interface ChartGroup { category: string; rows: any[]; } interface ChartSeries { name: string; data: (number | null)[]; /** Value of the split-by column this series stands for; null when unsplit. */ split_value: string | null; /** Rows behind each category of this series, aligned with `data`. */ cell_rows: any[][]; } interface ChartDataset { categories: string[]; groups: ChartGroup[]; series: ChartSeries[]; /** * The categories clubbed into the "Others" mark, when a top-N cut was made. * Carried so a drill on Others can filter by the real categories behind it — * nothing can be filtered by the literal label 'Others'. */ others_categories: string[]; } type ChartSort = 'none' | 'category_asc' | 'category_desc' | 'value_asc' | 'value_desc'; /** One step taken in a drill: which category, under which dimension. */ interface ChartDrillStep { /** Dimension whose mark was clicked — the one on screen at the time. */ dimension: string; /** The dimension now displayed as a result of this step. */ into: ChartDimension; /** The clicked mark's label; 'Others' when the clubbed mark was clicked. */ category: string; /** * Categories this step actually selects. One entry normally; every clubbed * category when 'Others' was clicked, since nothing matches that label. */ categories: string[]; /** Payload key to send `categories` under when a level re-queries. */ filter_field?: string; /** * Whether this step was answered by its own query rather than by narrowing the * rows already held. Recorded per step because a chain can mix the two: a * same-data step can sit under a queried one and vice versa. */ via_query: boolean; } interface ChartAggregateOptions { split_by?: string | null; sort?: ChartSort; /** * Plot only the highest N categories by measure and club the remainder into a * single 'Others' mark, so N = 5 draws 6. 0 or absent plots every category. */ top_n?: number; } declare const CHART_AGGREGATIONS: ChartAggregation[]; /** Label for the mark that carries everything below the top-N cut. */ declare const CHART_OTHERS_LABEL = "Others"; /** * A column's value on a row. Dotted paths are resolved so a nested result * (`customer.region`) can be a dimension, the same as the grid allows. */ declare function resolveChartValue(row: any, path: string): any; /** * A row's category under one dimension, in the exact form * `aggregateChartData` groups by. * * Exported so a drill can filter rows by a category it was handed back: doing * that with a raw `row[dim] === category` comparison misses every value the * grouping coerced — a number, a date, a null that became '(blank)'. */ declare function chartCategoryValue(row: any, dimension: string): string; /** * Dimensions as authored. * * Accepts the editor's array of objects, a JSON string of the same (what a * page saved through the JSON panel carries), and a plain comma-separated list * of column names — that last form is what the grid's `pivot_rows` uses, and * accepting it means an author can paste one into the other. */ declare function parseChartDimensions(raw: any): ChartDimension[]; /** A top-N entry as authored: blank, 0 and junk all mean "no cap". */ declare function parsePositiveInt(value: any): number | undefined; declare function parseChartMeasures(raw: any): ChartMeasure[]; /** The label a selector shows for a dimension or measure. */ declare function chartMeasureLabel(measure: ChartMeasure | null): string; declare function chartDimensionLabel(dimension: ChartDimension | null): string; /** * Group `rows` by one dimension and reduce one measure per group. * * When `split_by` names a column, one series is produced per distinct value of * that column — the chart equivalent of a pivot's column dimension. Otherwise * there is a single series named after the measure. * * Categories keep first-seen order unless a sort is asked for, so a query that * already ordered its rows (by month, say) plots in that order rather than * alphabetically. */ declare function aggregateChartData(rows: any[], dimension: ChartDimension | null, measure: ChartMeasure | null, options?: ChartAggregateOptions): ChartDataset; /** Reduce one bucket of rows to the measure's single number. */ declare function reduceRows(rows: any[], measure: ChartMeasure | null): number | null; interface ChartOptionOverride { /** Dotted path into the ECharts option, e.g. `xAxis.axisLabel.rotate`. */ path: string; /** Value as authored; JSON when it parses, otherwise the raw string. */ value: any; } /** * Overrides as authored. A value is run through JSON.parse first so `true`, `12`, * `"#fff"` and `{"fontSize":14}` all arrive as the type ECharts expects, falling * back to the plain string for bare words like `dashed`. */ declare function parseChartOptionOverrides(raw: any): ChartOptionOverride[]; /** * Apply overrides to a built option, in order. * * Mutates a copy, never the caller's object. A numeric path segment addresses an * array slot — `series.0.label.show` — which is how anything inside a series is * reached. Missing intermediates are created, choosing an array or an object * from the next segment so `series.0` does not silently build `{"0": …}`. */ declare function applyChartOptionOverrides(option: Record, overrides: ChartOptionOverride[]): Record; /** One value→colour row: a category as plotted, and the colour to paint it. */ interface ChartValueColor { value: string; color: string; } declare function parseChartValueColors(raw: any): ChartValueColor[]; /** * Value→colour pairs out of an entity field's own definition. * * A status field stores `open_status` / `close_status`, a tag or select field * stores `options`; both hold `{name|label, color}` — the same shapes the status * and tag components read. Pulling from here is what makes a chart agree with * every badge and cell showing the same value. */ declare function entityFieldValueColors(field: any): ChartValueColor[]; /** * ECharts label formatter for a label-content choice. * * `{b}` is the category, `{c}` the value and `{d}` the share of the total — * percent is only meaningful where ECharts computes one, so a caller that has no * total (a bar or line) should not offer the percent choices. */ declare function chartLabelFormatter(content: string): string | undefined; /** Which mark the user clicked, for the drill payload. */ type ChartKind = 'pie' | 'bar' | 'line'; /** * Drill support shared by the pie, bar and line charts. * * ECharts raises one `click` per rendered mark — a pie slice, a bar, a line's * symbol — and hands back which series and which index it was. That is all a * drill needs, so the charts do not have to hit-test anything themselves. * * The payload deliberately mirrors the grid's 'On Drill' (see * `grid.component.ts`): the record goes in `entity_data` and the click detail is * namespaced under a single `drill` key, so a handler written for one reads the * same way for the other and `set-from-payload` paths stay stable. */ declare abstract class ChartBaseComponent extends BaseComponent { componentEvents: { label: string; value: string; }[]; protected readonly containerStyles: _angular_core.Signal>; /** * The rows the chart was plotted from, in plot order. * * Held so a click can hand back the record behind the mark rather than only * its label and height — that record is what a drill target filters by. The * series data itself cannot serve: by the time it reaches ECharts it has been * reduced to a name and a number, and every other column is gone. */ protected sourceRows: any[]; /** * Rows behind each plotted cell, as `[seriesIndex][dataIndex]`. * * A split-by chart draws one series per split value, so the category index * alone no longer identifies a bucket — 'North' in the 'Q1' column is a * different set of rows from 'South' in the same column. Populated only on * the aggregated path; `sourceRows` still answers for the flat one. */ protected sourceCells: any[][][]; /** * The rows currently plotted — already filtered to the drill level in view. * Each chart's mapping reads this. */ protected lastRows: any[]; /** * The unfiltered rows at the top of the drill, kept separately from * `lastRows` so stepping back up does not need a refetch. */ protected baseRows: any[]; /** Rows fetched per drill level, when the drill re-queries. */ private levelRows; /** Which drill level the in-flight fetch is for; null means the base rows. */ private pendingLevel; private drillPathSig; /** * Id of the in-flight designer preview fetch, so the response effect in each * chart can tell its own reply from another component's. */ protected lastRequestId: _angular_core.WritableSignal; /** Guards against double-binding when a chart re-initialises. */ private drillBound; /** Watches html/body for the host app's theme swap. */ private themeObserver?; private themeMedia?; private themeMediaListener?; /** Resolved colours the chart last drew with, so a redraw is only done if needed. */ private lastThemeFingerprint; protected bindChartDrill(chart: echarts.ECharts, kind: ChartKind): void; /** * A mark was clicked. * * Ignores clicks that are not on a data mark — a legend entry, an axis label * and the title all raise `click` too, and none of them identifies a record. */ private onChartDrill; /** * Rows for a chart fed from a page state variable. * * The alternative — the chart's own `query` property — sends the query name * and a `transform` string to the server and plots whatever comes back, so a * chart has to restate a fetch the page has usually already made. Reading * state instead lets one `on_load` call-query feed several charts, and reuses * the page's existing update-state plumbing rather than a second path. * * Called from each chart's constructor, which is an injection context. */ protected watchStateRows(): void; /** Serialised form of the rows last handed to the chart. */ private lastAppliedRows; /** The `query#transform` last asked for, so the same fetch is not repeated. */ private lastQueryFingerprint; /** * Rows for a chart fed by its own saved query. * * The fetch used to be triggered only by the property panel's * `on_value_change`, which fires when an author edits the query — so at * runtime nothing ever asked for the rows and the chart drew its authored * placeholder instead. `store.queryData()` did not save it either: that map is * only filled as a side effect of some other component happening to fetch the * same query. * * Called from each chart's constructor, which is an injection context. */ protected watchQueryRows(): void; /** * Ask the host for this chart's query rows. Shared by the load-time effect and * the panel's on_value_change so an author's edit and the effect that observes * it cannot both fire the same fetch. */ protected requestQueryRows(query: string, transform: string): void; /** The function name last asked for, so the same fetch is not repeated. */ private lastFunctionFingerprint; /** * Rows for a chart fed by an eru function. * * The function is executed once and whatever it answers with is unwrapped by * `extractQueryRows`, exactly as a query response is — so Result Path works * the same way for both. * * Called from each chart's constructor, which is an injection context. */ protected watchFunctionRows(): void; /** Ask the host to run this chart's function. Mirrors requestQueryRows. */ protected requestFunctionRows(functionName: string): void; /** * Rows out of a saved query's response. * * The endpoint does not always answer with a bare array — a result is often * nested under the query's own name — so unwrap the same way the grid does, * with `query_result_path` as the explicit override when a response nests * deeper than one level. */ protected extractQueryRows(data: any): any[]; /** Keys a saved query's response wraps its row array in. */ private static readonly RESULT_KEYS; /** Hand rows to the chart's own mapping (its `updateChartData`). */ protected abstract applyRows(rows: any[]): void; /** The chart's authored static rows. Overridden by each chart. */ protected staticRows(): any[]; readonly inlineDrillEnabled: _angular_core.Signal; readonly drillPath: _angular_core.Signal; readonly drillDepth: _angular_core.Signal; /** Crumbs for the trail above the chart; index is the depth to return to. */ readonly drillCrumbs: _angular_core.Signal<{ label: string; index: number; }[]>; /** * The dimension actually plotted: the selector's pick at the top, the drill * level's own dimension once drilled. */ protected activeDimension(): ChartDimension | null; /** * The measure never changes on the way down. A drill answers "what is this * total made of", so re-measuring mid-drill would compare unlike numbers and * the parent's slice would stop accounting for its children. */ protected activeMeasure(): ChartMeasure | null; /** * The dimension a click on `from` opens, or null when it defines no drill. * * The target inherits the drill configuration of its own entry in the * dimensions list when it has one, which is what lets a drill run deeper than * one level without a separate chain to maintain: ds → an, and if `an` is also * a dimension with a drill of its own, the next click keeps going. */ protected drillTargetFor(from: ChartDimension | null): ChartDimension | null; /** * Rows for the level in view, resolved one step at a time so a chain can mix * the two kinds of step: a queried level stands on its own (the server already * answered for the whole path), a same-data level narrows the level above it. */ protected currentRows(): any[]; private rowsAtDepth; private unfilteredRows; /** * New rows arrived for the top of the chart. Any drill path is abandoned: its * steps describe categories from the previous result set. */ protected setBaseRows(rows: any[]): void; protected applyCurrentRows(): void; /** Step one level down from the clicked mark. */ private stepIntoDrill; /** Jump back to a depth from the breadcrumb. */ onDrillCrumb(index: number): void; /** * Fetch one drill level's own query, carrying every category clicked on the * way down so the level below is filtered by the whole path, not just the last * click. A clubbed 'Others' step sends its categories as an array for the * query to match with IN. */ private fetchDrillLevel; /** * Route a query response to whatever asked for it — the chart's own query, or * one drill level's. Shared here because all three charts had an identical * copy of this effect. */ protected watchQueryResponses(): void; /** * Where a chart takes its rows from. Spread into each chart's own schema so * the three offer the same choice in the same words. */ protected chartSourceSchema(): PropertySchema[]; /** Dimension picked in the selector. Empty means "whatever is first". */ private dimensionOverride; private measureOverride; readonly chartDimensions: _angular_core.Signal; readonly chartMeasures: _angular_core.Signal; /** * Falls back to the first configured entry rather than holding an index: the * author can reorder or delete entries in the panel while a chart is * selected, and an index would then point at a different column. */ readonly selectedDimension: _angular_core.Signal; readonly selectedMeasure: _angular_core.Signal; readonly selectedDimensionName: _angular_core.Signal; readonly selectedMeasureName: _angular_core.Signal; /** Whether the selector row should be offered at all. */ readonly showChartSelectors: _angular_core.Signal; /** * True once a dimension is configured. The charts keep their original * single-column mapping (`nameKey`/`valueKey`, `xAxisKey`/`yAxisKey`) for * pages saved before this existed, so the two paths have to be told apart. */ protected hasChartMapping(): boolean; /** * Whether rows are due to arrive from a query or page state. * * Charts fall back to their authored static data when nothing live is * configured, which is what makes a freshly dropped chart previewable. Once a * query or state key IS configured that fallback is wrong: it painted the * placeholder while the fetch was still in flight, so the chart showed * invented categories that no drill or selector could explain. */ protected hasLiveSource(): boolean; onDimensionSelected(name: string): void; onMeasureSelected(name: string): void; /** Re-run the chart's own mapping over the rows it already holds. */ protected replot(): void; /** * Group and reduce `rows` for the current dimension/measure, and record what * each mark was built from so a drill click can hand the records back. */ protected buildDataset(rows: any[]): ChartDataset; /** * The top-N cap in force. Held per dimension rather than per chart: cardinality * belongs to the column, so the cap that suits an eight-stage status is wrong * for a three-value type. Undefined plots every category. */ protected resolvedTopN(): number | undefined; /** The dataset last plotted, for the drill payload's split value. */ protected lastDataset: ChartDataset | null; /** The split column's value for a series, for the drill payload. */ private splitValueFor; /** * The legend block for a chart whose natural default is `fallback`. * `default` keeps each chart's established look, so existing pages do not * shift when this property appears. */ protected legendOption(fallback: 'none' | 'top' | 'bottom' | 'left' | 'right'): Record; /** Series colours, when the author has named a palette. */ protected paletteOption(): string[] | undefined; /** * The label block for a chart's marks. * * `default` keeps the chart's established look; a named position both turns * labels on and says where they go, so there is no second "show" switch to * contradict it. A page saved with the old `show_data_labels` boolean is still * honoured. */ protected dataLabelOption(default_show: boolean, default_position: string): Record; /** Title block, so all three charts size and place it the same way. */ protected titleOption(text: string): Record; /** * Colour per plotted category. * * The data model already says what colour a status or tag is, and a chart that * picks its own palette contradicts every badge and grid cell showing the same * value. Author rows win over the model so a one-off can still be forced. */ protected valueColors(): Record; /** * A field's definition, for its option colours. An empty entity falls back to * the page's own, which is what `getEntityField` already does. */ private entityFieldDefinition; /** * Redraw when the theme changes, so a token-coloured chart follows it. * * Two triggers are needed. The host app swaps classes/attributes on html/body, * which no signal exposes — hence the observer, the same one the grid uses. But * this theme's tokens are `light-dark(...)` pairs, so a change of the OS colour * scheme repaints every token with no DOM mutation at all; only the media query * sees that one. * * Called from each chart's constructor. */ protected watchThemeChanges(): void; /** The resolved colours this chart is currently drawing with. */ private themeFingerprint; /** * Whether the colours this chart actually uses now resolve differently. * Fingerprints the resolved values rather than the theme itself, so a chart * coloured entirely in literal hex never redraws. * * The baseline is taken when the watch is installed, not on the first * mutation — treating the first mutation as the baseline swallowed the first * theme switch after load, which is the only one most people try. */ private themeColorsChanged; ngOnDestroy(): void; /** * A design token resolved to a literal colour. * * ECharts draws on a canvas, and canvas cannot read CSS variables: handed * `var(--studio-primary)` it silently paints black. Nor can it parse what the * token actually holds — this theme stores `light-dark(oklch(…), oklch(…))`, * which canvas also rejects. Letting the browser compute it on a probe element * is what turns either into the `rgb(…)` canvas understands, and probing inside * this chart's own element means a scoped override still applies. */ protected resolveThemeColor(color: string): string; /** Whether any value colouring is configured at all. */ protected hasValueColors(): boolean; /** * Paint mapped colours onto plotted items. Each item carries its own * `itemStyle`, which is how ECharts colours by value rather than by series. */ protected withValueColors(items: T[]): any[]; /** * Formats the measure in view — for labels, tooltips and the value axis. * * The format comes from the data model first (the measure's own column unless * another is named), with the measure row's own settings as the override. Same * rule the number and currency fields follow, through the same helper, so a * currency column reads identically on a chart, a field and a grid. */ protected measureFormatter(): (value: any) => string; /** * A label callback that prints the chosen content with the measure formatted. * * A function rather than ECharts' `{c}` token because the token prints the raw * number — the whole point of mapping a format is that 12881829 reads as * ₹ 1.29 Cr. */ protected labelFormatterFor(content: string): ((params: any) => string) | undefined; /** Value-axis tick formatter, so the axis agrees with the labels. */ protected axisValueFormatter(): (value: any) => string; /** Title / label / legend text sizing. 0 or blank leaves the ECharts default. */ protected fontSize(key: 'title_font_size' | 'label_font_size' | 'legend_font_size'): number | undefined; /** Padding that keeps the legend off the plot. */ protected legendGap(): number; /** The legend's resolved side, for callers that must lay out around it. */ protected legendSide(fallback: string): string; /** Axis split lines — what "gridlines" means to a reader, unlike `grid.show`. */ protected showGridlines(): boolean; /** * Last step before handing an option to ECharts: apply the author's path/value * overrides. Applied last on purpose — an override should be able to beat any * property the panel set. */ protected withExtraOptions(option: Record): Record; /** * Appearance properties every chart offers. `include_axes` is false for the * pie, which has no axes to draw lines on or labels to rotate. */ protected chartAppearanceSchema(options?: { include_axes?: boolean; label_positions?: { value: string; label: string; }[]; label_contents?: { value: string; label: string; }[]; }): PropertySchema[]; /** * The dimension / measure configuration, shared by all three charts. * * `include_split` is false for the pie: a pie has one ring, so a second * dimension across the top has nowhere to go. */ protected chartMappingSchema(include_split?: boolean): PropertySchema[]; private rowForIndex; /** Every row behind the clicked mark, not just the representative one. */ private rowsForIndex; /** * The number behind the mark. A bar or line point arrives as a bare number, * a pie slice as `{name, value}`, so `params.value` alone is not enough. */ private numericValue; static ɵfac: _angular_core.ɵɵFactoryDeclaration; static ɵdir: _angular_core.ɵɵDirectiveDeclaration; } /** * The dimension / measure pickers drawn above a chart. * * Only shown when there is a choice to make: with one dimension and one measure * the chart has nothing to switch between and the row would be chrome. Native * selects rather than Material ones — this sits inside the plotted card at * runtime, and a mat-form-field's density and ripple fight the chart's own * padding for no gain on a two-item list. */ declare class ChartSelectorsComponent { dimensions: _angular_core.InputSignal; measures: _angular_core.InputSignal; selectedDimension: _angular_core.InputSignal; selectedMeasure: _angular_core.InputSignal; crumbs: _angular_core.InputSignal<{ label: string; index: number; }[]>; dimensionChange: _angular_core.OutputEmitterRef; measureChange: _angular_core.OutputEmitterRef; crumbClick: _angular_core.OutputEmitterRef; /** * Offer the slicing picker only at the top of a drill: below it the level * decides what is grouped, so the dropdown would contradict the chart. */ showDimension(): boolean; label(dimension: ChartDimension): string; measureLabel(measure: ChartMeasure): string; static ɵfac: _angular_core.ɵɵFactoryDeclaration; static ɵcmp: _angular_core.ɵɵComponentDeclaration; } declare class LineChartComponent extends ChartBaseComponent implements OnInit, OnDestroy { chartInstance: echarts.ECharts; chartOptions: echarts.EChartsCoreOption; initOptions: { renderer: "canvas"; useDirtyRect: boolean; }; constructor(); protected applyRows(rows: any[]): void; ngOnInit(): void; ngOnDestroy(): void; onChartInit(chart: echarts.ECharts): void; private parseJsonData; /** Axis and series mapped from live rows — see the note in the pie chart: * writing these back through setProperty() is a no-op at runtime. */ private mappedX; private mappedSeries; protected updateChartData(chartData: any): void; /** See the note on the bar chart's mappingRows(). */ private mappingRows; protected staticRows(): any[]; /** * One line per series of the grouped dataset. A Split By column gives a line * per split value; without one there is a single line for the measure. */ private mapDataset; private updateChart; protected getComponentPropertySchema(): PropertySchema[]; protected getComponentDefaultProperties(): any; static ɵfac: _angular_core.ɵɵFactoryDeclaration; static ɵcmp: _angular_core.ɵɵComponentDeclaration; } declare class BarChartComponent extends ChartBaseComponent implements OnInit, OnDestroy { chartInstance: echarts.ECharts; chartOptions: echarts.EChartsCoreOption; initOptions: { renderer: "canvas"; useDirtyRect: boolean; }; constructor(); protected applyRows(rows: any[]): void; ngOnInit(): void; ngOnDestroy(): void; onChartInit(chart: echarts.ECharts): void; private parseJsonData; /** Axis and series mapped from live rows — see the note in the pie chart: * writing these back through setProperty() is a no-op at runtime. */ private mappedX; private mappedY; /** One entry per series when a Split By column fans the categories out. */ private mappedSeries; protected updateChartData(chartData: any): void; /** * The rows to group: whatever a query or page state last delivered, falling * back to the authored static rows so the designer previews a chart with * dimensions the same way it will draw at runtime. */ private mappingRows; protected staticRows(): any[]; private updateChart; protected getComponentPropertySchema(): PropertySchema[]; protected getComponentDefaultProperties(): any; static ɵfac: _angular_core.ɵɵFactoryDeclaration; static ɵcmp: _angular_core.ɵɵComponentDeclaration; } declare class PieChartComponent extends ChartBaseComponent implements OnInit, OnDestroy { chartInstance: echarts.ECharts; chartOptions: echarts.EChartsCoreOption; initOptions: { renderer: "canvas"; useDirtyRect: boolean; }; constructor(); protected applyRows(rows: any[]): void; ngOnInit(): void; ngOnDestroy(): void; onChartInit(chart: echarts.ECharts): void; private parseJsonData; /** * Slices mapped from live rows. * * Held here rather than written back through `setProperty('data', …)`: that * write goes to the component's stored properties, which is a no-op at * runtime, so `updateChart()` read the untouched default straight back and * every chart plotted its placeholder data instead of the query's. */ private mappedSlices; protected updateChartData(chartData: any): void; /** * Rows either already carry `name`/`value` (what updateChartData writes back, * and the designer default) or are raw result rows with `nameKey`/`valueKey` * naming the columns to plot — the shape a page-generating agent emits in the * static `data` property. Map the latter here, so static data does not have to * come through updateChartData() to be plottable. */ private toSeriesData; /** * The rows to group: whatever a query or page state last delivered, falling * back to the authored static rows so the designer previews a chart with * dimensions the same way it will draw at runtime. */ private mappingRows; protected staticRows(): any[]; /** * Where the pie sits. ECharts centres it regardless of the legend, so a * vertical legend overlaps the slice labels on that side; nudging the centre * the other way is what buys the gap the reader expects. */ private pieCenter; private updateChart; protected getComponentPropertySchema(): PropertySchema[]; static ɵfac: _angular_core.ɵɵFactoryDeclaration; static ɵcmp: _angular_core.ɵɵComponentDeclaration; } declare class EruPageComponent extends BaseComponent implements OnInit, OnDestroy { targetPageId?: string; displayMode: 'popup' | 'side_panel' | 'inline'; buttonText?: string; buttonIcon?: string; masterDetailConfig?: MasterDetailConfig; dataContext?: Record; autoOpen?: boolean; pageOpened: EventEmitter<{ pageId: string; data: any; }>; pageClosed: EventEmitter; dataChanged: EventEmitter<{ pageId: string; data: any; }>; protected store: EruStudioStore; readonly isPageOpen: _angular_core.WritableSignal; readonly availablePages: _angular_core.WritableSignal; readonly selectedPageId: _angular_core.WritableSignal; readonly isLoading: _angular_core.WritableSignal; readonly buttonDisplayText: _angular_core.Signal; readonly buttonDisplayIcon: _angular_core.Signal; readonly isDisabled: _angular_core.Signal; readonly canOpenPage: _angular_core.Signal; readonly currentPageTitle: _angular_core.Signal; constructor(); ngOnInit(): void; ngOnDestroy(): void; /** * Load available pages from the store */ private loadAvailablePages; /** * Open the selected page */ openPage(): void; /** * Close the page */ closePage(): void; /** * Handle page selection change */ onPageSelectionChange(pageId: string): void; /** * Handle display mode change */ onDisplayModeChange(mode: 'popup' | 'side_panel' | 'inline'): void; /** * Get button text based on context */ private getButtonText; /** * Get button icon based on context */ private getButtonIcon; /** * Get page title by ID */ getPageTitle(pageId: string): string; /** * Get default width based on display mode */ private getDefaultWidth; /** * Get default height based on display mode */ private getDefaultHeight; readonly displayModes: Array<{ value: string; label: string; icon: string; }>; /** * Get data context keys for template */ getDataContextKeys(): string[]; static ɵfac: _angular_core.ɵɵFactoryDeclaration; static ɵcmp: _angular_core.ɵɵComponentDeclaration; } /** * Page Component - A container component that can hold child components. * This component serves as a page section that allows page definitions to include * other components as children, similar to FlexContainerComponent. */ declare class PageRefComponent extends BaseComponent implements OnDestroy { /** * Width a side panel takes when nothing else decides it. Also the floor for * an auto-sized one, and the fallback the pinned-panel body gutter uses. */ private static readonly DEFAULT_SIDE_PANEL_WIDTH; componentEvents: { label: string; value: string; }[]; private static readonly EMPTY_ARRAY; /** A value as an array, parsing JSON text — a query can return an array * column as a string. Null when it is neither. */ private static asArray; private componentRegistry; private readonly parentEnv; private readonly dialog; private readonly overlay; private overlayRef; /** * Breakpoint scope handed to the nested page's components, measured from * the box this reference actually occupies — the inline slot, or the panel * body once an overlay opens. The nested page keeps sharing this store, so * this is the only thing that has to be scoped for a panel to lay itself * out for its own width instead of the browser window's. * * Deliberately not conditional on edit mode: a reference's content is not * authored from the host page (you open the nested page on its own to edit * it, and there the toolbar's breakpoint applies), so the designer should * show the same layout the reference will really render. */ private readonly nestedBreakpointScope; private stopObservingWidth; readonly dynamicComponentInjector: EnvironmentInjector; /** * Signal-backed input for nesting level. * Using @Input with a setter to avoid InputSignal brand mismatch issues in library consumers. */ readonly nestingLevel: _angular_core.WritableSignal; set nestingLevelSetter(value: number); readonly selectedComponent: _angular_core.Signal; readonly componentsReady: _angular_core.WritableSignal; /** * For popup / side_panel page_refs that are NOT auto_open, we don't * fetch the target page def until the user actually opens the panel. * Set to true on the first `openPanel` call and stays true (refetching * after user closes is unnecessary — the def is the same). */ private readonly hasBeenOpened; readonly selectedPage: _angular_core.Signal; readonly selectedPageEntity: _angular_core.Signal; readonly nestingType: _angular_core.Signal; readonly display_type: _angular_core.Signal; readonly loopSource: _angular_core.Signal; /** * Key of the array inside the hosting record, for `nested_array`/ * `nested_object`. The Entity picker offers the nested page's entity name * and the framework has always used that as the JSON key too, which only * holds while the data is shaped by the data model. A record built by a * query carries the alias the query chose (`txns`), so Array Field names it * explicitly and falls back to the entity for every page authored so far. */ readonly arrayFieldKey: _angular_core.Signal; readonly loopMatchFields: _angular_core.Signal; readonly entityIdKey: _angular_core.Signal; readonly dataSource: _angular_core.Signal; readonly apiName: _angular_core.Signal; readonly functionName: _angular_core.Signal; readonly queryName: _angular_core.Signal; readonly apiPayloadFields: _angular_core.Signal; readonly stateScope: _angular_core.Signal<"page" | "app">; readonly stateField: _angular_core.Signal; /** Fingerprint of the last applied state-source data (see the state effect). */ private lastStateSourceKey; /** Nested page + refresh tick an inline ref last fired `on_load` for. */ private firedInlineOnLoadKey; private lastOnLoadKey; private lastApiCallKey; private lastApiErrorReqId; private _refreshTick; private _loopApiData; private _loopFieldData; private _pageRefApiData; private lastRequestIdMap; readonly activePageRefDataReqId: _angular_core.Signal; readonly nestedPage: _angular_core.Signal; readonly isPageLoading: _angular_core.Signal; readonly loopSourceData: _angular_core.Signal; readonly arrayData: _angular_core.Signal; nestedPageComponents: _angular_core.Signal; private getNestedComponentsForIndex; /** * A looped record card is not a page, so the nested page's own full-height * root sizing must not come along: a root authored `min-height: 100vh` (or * `h-full`) claims a whole viewport PER RECORD, which is how two cards ended * up 953px tall around 311px of content with the blank space read as a gap. * Only viewport-relative heights and the full-height classes are dropped — * every other authored style is left exactly as it is. */ private static withoutViewportHeight; /** * Memo for the generated loop tree — see displayComponents. */ private lastDisplay; readonly displayComponents: _angular_core.Signal; private resolveFieldOptions; private padPageDataArrayWithLoopSource; private normalizeLoopItem; private extractLoopRecordId; private getDefaultRow; private padNestedArrayWithLoopSource; private createFlexContainer; /** * One wrapper per looped record, so a nested page with more than one * top-level component keeps its components together instead of interleaving * them with the next record's as flat siblings of the loop container. * * It is also the only place a record can be given its share of the main * axis. `width: 100%` is deliberate rather than a flex shorthand: the parent * flex container already translates a full width into `flex-grow: 1; * flex-basis: 0%; min-width: 0` when it lays out in a row, and into a plain * width when it lays out in a column — which is exactly the reading wanted * in each direction. Without it a card sized to its content and left the * rest of the row empty. */ private createRecordCard; /** * Width handed to each looped record card, as the flex container reads it: * `100%` becomes an equal share of the row, `auto` lets the card size to its * own content, and anything else is pinned (flex-basis, no grow/shrink). */ private loopCardWidth; protected isChildHovered: _angular_core.Signal; constructor(); ngOnDestroy(): void; /** * Point the nested page's breakpoint scope at whichever box currently holds * it. Inline that is this component's own element; for a popup or side panel * it is the overlay's body, which lives in a different DOM tree and only * exists while the panel is open. */ private observeNestedWidth; /** * Whether the nested page loops an array laid out as columns. * * This is the case a fixed panel width cannot serve: the panel holds N cards * in a row, so the width it needs is a property of the data, not of the * design. Anything else has one card's worth of content and a fixed width is * the right answer. */ private loopsSideBySide; /** The author's Panel Width, trimmed. Blank means "decide for me". */ private authoredPanelWidth; openPanel(): void; private openPopup; private openSidePanel; private resolvedPanelTitle; panelTriggerLabel(): string; onDragOver(event: DragEvent): void; selectComponent(event: MouseEvent, component: EruComponent): void; onComponentDragStart(event: DragEvent, component: EruComponent): void; onComponentDragEnd(event: DragEvent): void; onComponentMouseEnter(event: MouseEvent, component: EruComponent): void; onComponentMouseLeave(event: MouseEvent): void; onCanvasMouseOver(event: MouseEvent, component: EruComponent): void; isContainer(type: string): boolean; hasCustomComponent(type: string): boolean; getComponentClass(type: string): any; getZIndex(level: number): number; /** * Whether a data source already fires the nested page's `on_load`. * * The two data-arrival paths below fire it as they apply their record, so a * ref that fetches or reads state is covered. A ref with neither — the * nested page fetches its own data in its own `on_load` — had nothing at * all firing it: the page worked opened directly (RuntimeCanvas fires it on * mount) and was silent inside a page_ref. */ private readonly onLoadFiredByDataSource; /** * Fire `on_load` for a nested page no data source speaks for. A panel fires * on every open, not once per mount: the payload an on_load action sends is * read when it runs, and a panel is usually reopened precisely because the * state behind it changed. */ private fireUnsourcedOnLoad; private firePageEvents; private resolveApiPayload; private applyQueryResultPath; private resolveValuePath; private normalizeObjectResponse; private normalizeArrayResponse; private isComponentIdAction; protected getPropertySchema(): PropertySchema[]; static ɵfac: _angular_core.ɵɵFactoryDeclaration; static ɵcmp: _angular_core.ɵɵComponentDeclaration; } /** * Widget Component - A component that can host a dynamically loaded widget. * Similar to PageRefComponent, it fetches a widget definition based on selection. */ declare class WidgetComponent extends BaseComponent { private componentRegistry; private readonly parentEnv; readonly dynamicComponentInjector: EnvironmentInjector; readonly nestingLevel: _angular_core.InputSignal; readonly selectedComponent: _angular_core.Signal; readonly componentsReady: _angular_core.WritableSignal; readonly selectedWidget: _angular_core.Signal; private lastRequestId; readonly nestedWidgetData: _angular_core.Signal; readonly displayComponents: _angular_core.Signal; constructor(); selectComponent(event: MouseEvent, component: EruComponent): void; onComponentMouseEnter(event: MouseEvent, component: EruComponent): void; onComponentMouseLeave(event: MouseEvent): void; isContainer(type: string): boolean; getComponentClass(type: string): any; getZIndex(level: number): number; getChildStyles(child: EruComponent): any; protected getComponentPropertySchema(): PropertySchema[]; static ɵfac: _angular_core.ɵɵFactoryDeclaration; static ɵcmp: _angular_core.ɵɵComponentDeclaration; } declare class CanvasComponent implements OnInit, OnDestroy { store: EruStudioStore; componentRegistry: ComponentRegistryService; schemaService: ComponentSchemaService; tailwindService: PropertyToTailwindService; formValidationService: FormValidationService; logicEvaluator: LogicEvaluatorService; private readonly hostElement; protected pageId: _angular_core.Signal; private readonly parentEnv; readonly dynamicComponentInjector: EnvironmentInjector; readonly componentsReady: _angular_core.WritableSignal; /** * Signal-backed input for initial entity ID. * Using @Input with a setter to avoid InputSignal brand mismatch issues in library consumers. */ readonly initialEntityId: _angular_core.WritableSignal; set initialEntityIdSetter(value: string); readonly savePage: _angular_core.OutputEmitterRef; readonly publishPage: _angular_core.OutputEmitterRef; private readonly pageDataIdentity; private readonly lastPageDataRequestId; /** Page the in-flight ds_page_data fetch was issued for. A navigation can * land before the response does, and the record must not be applied to * whatever page happens to be current by then. */ private readonly pageDataRequestPageId; private firedOnLoadPageId; /** Last seen edit-mode value, so the effect below reacts to the transition. */ private wasEditMode; /** * The breakpoint the author was authoring against, parked while preview * drives the breakpoint off the real window width. * * Coming back from preview used to leave the detected breakpoint in place on * the grounds that it was "the author's choice" — but it is not: preview * overwrote the author's choice on the way out. The author then carried on * editing against, say, `xl`, where every responsive property they touched * was written into an xl override and every one they read resolved to xl * instead of base. `hide_columns` made that visible: a single Hidden tick at * xl wrote a one-name xl array that shadowed the base list entirely, so all * the other hidden columns reappeared. */ private editBreakpoint; constructor(); readonly breakpoints: _angular_core.Signal<{ name: string; label: string; width: number; icon: string; }[]>; readonly currentBreakpoint: _angular_core.Signal; readonly currentZoom: _angular_core.WritableSignal; readonly components: _angular_core.Signal; readonly selectedComponent: _angular_core.Signal; readonly editMode: _angular_core.Signal; readonly showComponentPalette: _angular_core.WritableSignal; readonly showPropertiesPanel: _angular_core.WritableSignal; readonly showJsonEditor: _angular_core.WritableSignal; readonly showPageStatePanel: _angular_core.WritableSignal; getFormControlsDebugInfo(): any; protected isComponentHovered: _angular_core.Signal<(component_id: string | null) => boolean>; private draggedComponentId; ngOnInit(): void; ngOnDestroy(): void; onWindowResize(): void; onKeyDown(event: KeyboardEvent): void; private regenerateIds; private updateBreakpointFromViewport; setBreakpoint(breakpoint: string): void; calculateBreakpointFromViewport(): string; toggleEditMode(): void; onDragOver(event: DragEvent): void; onDrop(event: DragEvent): void; private flattenComponents; private moveComponentBeforeTarget; private findComponentRecursively; private moveComponentWithinContainer; private moveComponentBetweenContainers; private createComponentInContainer; selectComponent(event: MouseEvent, component: EruComponent): void; onComponentSelect(event: CustomEvent): void; private findComponentById; onCanvasClick(event: MouseEvent): void; getCanvasLayoutClass(): string; getPropertyValue(component: EruComponent, key: string, defaultValue?: any): any; getDataKey(component: EruComponent): string; isChildVisible(component: EruComponent): boolean; getChildStyles(component: EruComponent): any; /** * Types that render their children themselves and therefore need the canvas * contract passed down — a container id to drop into, and a nesting level. * * `attachment` is here for its panel-header slot. Without the nesting level * the slot and everything in it computed their depth from 0, so an attachment * sitting a few levels deep came out DEEPER than its own children, and the * click handler — which picks the highest `data-nesting-level` containing the * target — selected the attachment instead of the component inside the * header. It only looked right at the top level, where the numbers happened * to line up. */ isContainer(type: string): boolean; onComponentDragStart(event: DragEvent, component: EruComponent): void; onComponentMouseLeave(event: MouseEvent): void; onCanvasMouseOver(event: MouseEvent, component: EruComponent): void; onComponentMouseEnter(event: MouseEvent, component: EruComponent): void; onComponentDragEnd(event: DragEvent): void; hasCustomComponent(type: string): boolean; getComponentClass(type: string): any; onSave(): void; onPublish(): void; getZIndex(level: number): number; static ɵfac: _angular_core.ɵɵFactoryDeclaration; static ɵcmp: _angular_core.ɵɵComponentDeclaration; } interface PanelSize { width: number; height: number; } interface PanelPosition { x: number; y: number; } declare class FloatingPanelComponent implements OnInit, AfterViewInit, OnDestroy { panelId: string; title: string; initialSize: PanelSize; initialPosition: PanelPosition; panelContainer: ElementRef; size: PanelSize; position: PanelPosition; isMinimized: boolean; private originalHeight; private isDragging; private offsetX; private offsetY; private mouseMoveHandler?; private mouseUpHandler?; ngOnInit(): void; ngAfterViewInit(): void; ngOnDestroy(): void; onMouseDown(event: MouseEvent): void; private onMouseMove; private onMouseUp; toggleMinimize(): void; private disableCanvasPointerEvents; private enableCanvasPointerEvents; private bringToFront; static ɵfac: _angular_core.ɵɵFactoryDeclaration; static ɵcmp: _angular_core.ɵɵComponentDeclaration; } declare class AssetsPanelComponent { static ɵfac: _angular_core.ɵɵFactoryDeclaration; static ɵcmp: _angular_core.ɵɵComponentDeclaration; } declare class LayersPanelComponent implements AfterViewInit, OnDestroy { private store; readonly treeViewport: _angular_core.Signal | undefined>; readonly treeEl: _angular_core.Signal | undefined>; readonly hscrollEl: _angular_core.Signal | undefined>; readonly hspacerEl: _angular_core.Signal | undefined>; readonly hscrollVisible: _angular_core.WritableSignal; private resizeObserver; private hscrollListener; private syncRafHandle; readonly components: _angular_core.Signal; readonly selectedComponent: _angular_core.Signal; readonly hoveredLayerId: _angular_core.WritableSignal; readonly searchTerm: _angular_core.WritableSignal; readonly filteredComponents: _angular_core.Signal; private filterNode; private nodeMatches; private draggedComponentId; /** * Name shown on the page row: the page's title, its name, else a neutral * label — a page saved without either should still be selectable. */ readonly pageLabel: _angular_core.Signal; /** * Clear the component selection, which is what makes the properties panel * fall back to the page's own properties. */ selectPage(): void; selectComponent(component: EruComponent): void; deleteComponent(componentId: string): void; onDragStart(event: DragEvent, component: EruComponent): void; onDragOver(event: DragEvent, component: EruComponent): void; onDragLeave(event: DragEvent): void; onDrop(event: DragEvent, targetComponent: EruComponent): void; onDragEnd(event: DragEvent): void; clearSearch(): void; onExpandToggled(): void; private scheduleSync; private runSync; constructor(); ngAfterViewInit(): void; ngOnDestroy(): void; private moveComponentBeforeTarget; static ɵfac: _angular_core.ɵɵFactoryDeclaration; static ɵcmp: _angular_core.ɵɵComponentDeclaration; } declare class LayerTreeItemComponent { component: _angular_core.InputSignal; level: _angular_core.InputSignal; selectedId: _angular_core.InputSignal; hoveredId: _angular_core.InputSignal; forceExpanded: _angular_core.InputSignal; componentSelected: _angular_core.OutputEmitterRef; componentDeleted: _angular_core.OutputEmitterRef; dragStarted: _angular_core.OutputEmitterRef<{ event: DragEvent; component: EruComponent; }>; draggedOver: _angular_core.OutputEmitterRef<{ event: DragEvent; component: EruComponent; }>; dragLeft: _angular_core.OutputEmitterRef; dropped: _angular_core.OutputEmitterRef<{ event: DragEvent; component: EruComponent; }>; dragEnded: _angular_core.OutputEmitterRef; expandToggled: _angular_core.OutputEmitterRef; private userExpanded; readonly expanded: _angular_core.Signal; toggleExpand(): void; hasChildren(): boolean; canDelete(): boolean; getIcon(): string; isContainer(): boolean; getName(): string; onSelect(event: Event): void; onDelete(event: Event): void; onDragStart(event: DragEvent): void; onDragOver(event: DragEvent): void; onDragLeave(event: DragEvent): void; onDrop(event: DragEvent): void; onDragEnd(event: DragEvent): void; static ɵfac: _angular_core.ɵɵFactoryDeclaration; static ɵcmp: _angular_core.ɵɵComponentDeclaration; } declare class PropertiesPanelComponent { private store; private registry; private tailwindService; private schemaService; readonly selectedComponent: _angular_core.Signal; readonly currentPage: _angular_core.Signal; readonly currentBreakpoint: _angular_core.Signal; readonly entities: _angular_core.Signal; entityInputControl: _angular_core.WritableSignal; filteredEntities: _angular_core.Signal; readonly componentDefinition: _angular_core.Signal; readonly propertySchema: _angular_core.Signal; readonly styleSchema: _angular_core.Signal; readonly isSchemaLoading: _angular_core.Signal; constructor(); getPropertyCategories(): PropertyCategory[]; getPropertiesByCategory(category: PropertyCategory): PropertySchema[]; /** * The visible properties of a category, each carrying the sub-heading to draw * above it (null when it continues the previous one). Computed here rather * than in the template because the heading depends on the *previous visible* * property — a `visible_if` that hides the first member of a section has to * move the heading onto whichever member survives. */ getPropertyRows(category: PropertyCategory): { schema: PropertySchema; section: string | null; }[]; getStyleCategories(): string[]; getStylesByCategory(category: string): StyleSchema[]; /** * The entity field the selected component is bound to, or null when it is * unbound. Mirrors BaseComponent.fieldValue() so the panel resolves the same * field the component itself reads from. */ private readonly boundEntityField; /** * Property keys whose studio name differs from the data-model key, with the * transform needed to show the model's value in the panel's control. Every * other inherited key matches by name and needs neither. */ private static readonly INHERITED_ALIASES; /** * Value to show for an inherited property, or undefined when the property is * not inherited (unbound component, or a key the data model does not own). * * This is what lets the panel stop *copying* model values into the page. The * control is already locked by disable_if for these keys; previously the only * way to show the real value was to write it into the component's stored * properties on bind, which left the page holding a snapshot that drifted as * soon as the data model changed. Reading it here instead means the model * stays the single source of truth and the panel still shows the truth. */ private static readonly SEEDED_KEYS; /** The component's own stored value for a key, ignoring any inheritance. */ private storedPropertyValue; /** * The field a tag's option list comes from: `options_from_field` when the * author pointed it at another field, otherwise the one it is bound to. */ private optionsSourceField; /** * `open_status` + `close_status` as the single list the options editor draws. * Entries keep the model's `name`, which the editor already reads alongside * `label`, so nothing is lost writing back. */ private statusGroupsAsOptions; private inheritedPropertyValue; readonly currentPropertyValue: _angular_core.Signal<(key: string) => any>; readonly hasBreakpointOverride: _angular_core.Signal<(_key: string) => boolean>; readonly isInheritedFromBase: _angular_core.Signal<(_key: string) => boolean>; clearBreakpointOverride(propertyName: string): void; /** * Which half of a read-only display the Typography group is editing. * * A view-mode control draws two pieces of text — its label and its value — * and the Styles tab owns the VALUE. Rather than doubling the group with a * second set of Font Size / Text Color / Font Weight controls, the same * controls are repointed: on 'label' every typography key is stored and read * with a `label_` prefix, which is what `BaseComponent.displayLabelStyles()` * renders. Same pattern as the Solid / Gradient / Image switch on * `background_color`, one group up. */ readonly styleTarget: _angular_core.WritableSignal<"label" | "value">; private readonly resetStyleTargetOnSelection; /** Style keys the target switch applies to — the Typography group. */ private static readonly TARGETABLE_STYLE_KEYS; /** * The stored key for a style control, given the active target. Anything * outside the typography set is never prefixed, so switching the target * cannot silently repoint padding or a background. */ private targetedStyleKey; /** * Whether the target switch is offered at all: only for a control that * actually draws a label, which is the ones declaring a `label` property. * A container, grid or chart has no second piece of text to point at. */ readonly supportsLabelTarget: _angular_core.Signal; setStyleTarget(target: 'value' | 'label'): void; readonly currentStyleValue: _angular_core.Signal<(key: string) => any>; updateProperty(propertyName: string, value: any): void; updateStyleProperty(propertyName: string, event: Event): void; updateStylePropertyAtBreakpoint(propertyName: string, value: any): void; clearStylePropertyAtBreakpoint(propertyName: string): void; /** True when the current (non-base) breakpoint has its own value for a style * key — i.e. a "remove this override, inherit base" action is meaningful. */ readonly hasStyleBreakpointOverride: _angular_core.Signal<(_key: string) => boolean>; private readonly GRADIENT_KEY; bgIsGradient(): boolean; private parseGradient; private writeGradient; setBgFillMode(mode: string): void; readonly bgImageMode: _angular_core.WritableSignal; private _lastSelectedId; bgIsImage(): boolean; getBgImage(): string; setBgImage(value: string): void; private clearBgImage; getBgImageOption(key: string, fallback: string): string; setBgImageOption(key: string, value: string): void; readonly bgSizeOptions: { label: string; value: string; }[]; readonly bgPositionOptions: { label: string; value: string; }[]; readonly bgRepeatOptions: { label: string; value: string; }[]; private lastRequestIdImages; readonly availableImages: _angular_core.Signal; bgImagePreview(): string; getGradientAngle(): number; setGradientAngle(v: any): void; private gradientStop; isGradientStopToken(i: 1 | 2): boolean; getGradientStopToken(i: 1 | 2): string; getGradientStopHex(i: 1 | 2): string; setGradientStopToken(i: 1 | 2, tokenValue: string): void; setGradientStopHex(i: 1 | 2, hex: string): void; gradientPreview(): string; readonly styleColorTokens: { label: string; value: string; }[]; /** Extract a `var(--studio-*)` token from a plain token value or a * color-mix(in srgb, N%, transparent) wrapper. Null if not a token. */ private extractToken; isStyleTokenColor(key: string): boolean; /** The token to preselect in the token dropdown ('' when custom). */ getStyleTokenValue(key: string): string; getStyleHexColor(key: string, defaultValue: string): string; /** Current alpha of a style color as a 0–100 percentage (100 = opaque). */ getStyleAlpha(key: string, defaultValue: string): number; updateStyleColorHex(key: string, hexInput: string): void; updateStyleOpacity(key: string, pctInput: string): void; private composeColor; /** Token color with optional opacity via color-mix (theme-reactive). */ private composeTokenColor; private parseColor; private toHex; onStyleColorTokenChange(key: string, tokenValue: string): void; readonly paddingValue: _angular_core.Signal; readonly marginValue: _angular_core.Signal; readonly gapValue: _angular_core.Signal; readonly borderValue: _angular_core.Signal; readonly roundedValue: _angular_core.Signal; readonly shadowValue: _angular_core.Signal; getResponsiveClasses(): string; updateResponsiveClasses(value: string): void; updateCommonClasses(value: string): void; customStyleEntries: _angular_core.Signal<{ key: string; value: any; }[]>; addCustomStyle(): void; updateCustomStyleKey(oldKey: string, newKey: string): void; updateCustomStyleValue(key: string, value: string): void; removeCustomStyle(key: string): void; otherStyleEntries: _angular_core.Signal<{ key: string; value: any; renderable: boolean; overridden: boolean; }[]>; addOtherStyle(): void; updateOtherStyleKey(oldKey: string, newKey: string): void; updateOtherStyleValue(key: string, value: string): void; removeOtherStyle(key: string): void; deleteComponent(): void; readonly copiedId: _angular_core.WritableSignal; copyId(id: string): void; private fallbackCopy; isPropertyVisible(schema: PropertySchema): boolean; isPropertyEditable(schema: PropertySchema): boolean; isPropertyDisabled(schema: PropertySchema): boolean; isFormField(): boolean; private _eventSubscriptions; readonly eventSubscriptions: _angular_core.Signal; readonly availableEvents: _angular_core.Signal<{ label: string; value: string; }[]>; readonly availablePayloadFields: _angular_core.Signal; /** * Column / aggregation keys an 'On Drill' subscription can name, supplied by * the selected grid. * * Offered as autocomplete suggestions rather than a fixed dropdown: a grid * bound to a page field or state has no columns until the parent query * returns, so at design time the list can legitimately be empty and the * author still has to be able to type the key. Same reason 'Hide Columns' * carries a manual text fallback. */ private lastRequestIdDrillKeys; readonly availableDrillKeys: _angular_core.Signal; /** Suggestions narrowed to what the author has typed so far. */ filteredDrillKeys(typed: string | undefined): { label: string; value: string; }[]; private lastRequestIdApis; readonly availableApis: _angular_core.Signal; private lastRequestIdFunctions; readonly availableFunctions: _angular_core.Signal; private lastRequestIdQueries; readonly availableQueries: _angular_core.Signal; /** * Function suggestions narrowed to what the author has typed. Offered as * autocomplete rather than a fixed dropdown so a function that does not * exist on the server yet can still be named here and written later. */ filteredFunctions(typed: string | undefined): { label: string; value: string; }[]; /** True when the typed name matches no known function, so 'Add "x"' is offered. */ isNewFunctionName(typed: string | undefined): boolean; /** * Query suggestions narrowed to what the author has typed. Offered as * autocomplete rather than a fixed dropdown so a query that does not exist * on the server yet can still be named here and written later. */ filteredQueries(typed: string | undefined): { label: string; value: string; }[]; /** True when the typed name matches no known query, so 'Add "x"' is offered. */ isNewQueryName(typed: string | undefined): boolean; private readonly PANEL_ID; private _lastReqIdPages; private _availablePages; readonly availablePages: _angular_core.Signal; readonly availableFields: _angular_core.Signal<{ label: string; value: string; }[]>; readonly stateSourceFields: _angular_core.Signal<{ label: string; value: string; }[]>; readonly availableComponents: _angular_core.Signal<{ label: string; value: string; }[]>; readonly availableSteppers: _angular_core.Signal<{ label: string; value: string; }[]>; readonly availableTabGroups: _angular_core.Signal<{ label: string; value: string; }[]>; /** * Every tab title on the page, labelled by the group it belongs to. The * action stores the title rather than a position, so renaming a tab in one * place does not silently repoint an action at its neighbour. */ readonly availableTabNames: _angular_core.Signal<{ label: string; value: string; }[]>; readonly availableGrids: _angular_core.Signal<{ label: string; value: string; }[]>; readonly availableSidePanels: _angular_core.Signal<{ label: string; value: string; }[]>; readonly availablePageRefs: _angular_core.Signal<{ label: string; value: string; }[]>; addEventSubscription(): void; removeEventSubscription(index: number): void; updateEventSubscription(index: number, field: keyof ComponentEventSubscription, value: any): void; addNavParam(index: number): void; updateNavParam(index: number, paramIndex: number, key: keyof NavParam, value: any): void; removeNavParam(index: number, paramIndex: number): void; addPayloadExtra(index: number): void; updatePayloadExtra(index: number, extraIndex: number, key: keyof NavParam, value: any): void; removePayloadExtra(index: number, extraIndex: number): void; addPageNavParam(index: number): void; updatePageNavParam(index: number, paramIndex: number, key: keyof NavParam, value: any): void; removePageNavParam(index: number, paramIndex: number): void; addOnSuccessAction(eventIndex: number): void; removeOnSuccessAction(eventIndex: number, actionIndex: number): void; updateOnSuccessAction(eventIndex: number, actionIndex: number, key: string, value: any): void; addOnErrorAction(eventIndex: number): void; removeOnErrorAction(eventIndex: number, actionIndex: number): void; updateOnErrorAction(eventIndex: number, actionIndex: number, key: string, value: any): void; readonly pageStateKeys: _angular_core.Signal; updateStateFormulaFn(index: number, fn: string): void; updateStateFormulaParam(index: number, param: keyof UpdateStateFormula, value: any): void; updateNestedStateFormulaFn(eventIndex: number, actionIndex: number, branch: 'on_success' | 'on_error', fn: string): void; updateNestedStateFormulaParam(eventIndex: number, actionIndex: number, branch: 'on_success' | 'on_error', param: keyof UpdateStateFormula, value: any): void; private saveEventSubscriptionsToComponent; updatePageName(name: string): void; updatePageTitle(title: string): void; updatePageDataSource(value: 'none' | 'state'): void; updatePageStateScope(value: 'page' | 'app'): void; updatePageStateField(value: string): void; updatePageStateResultPath(value: string): void; /** State variables declared on this page, for the page-scope field picker. */ readonly pageStateVarOptions: _angular_core.Signal; /** Keys currently present in app state, for the app-scope field picker. */ readonly appStateKeyOptions: _angular_core.Signal; updatePageEntityInput(value: string): void; onEntityOptionSelected(entityName: string): void; onEntityInputBlur(): void; updatePageEntity(entityName: string): void; readonly pageEventSubscriptions: _angular_core.Signal; readonly pageLifecycleEvents: { label: string; value: string; }[]; isPredefinedPageEvent(value: string | undefined): boolean; onPageEventTypeChange(index: number, value: string): void; addPageEventSubscription(): void; updatePageEventSubscription(index: number, field: string, value: any): void; removePageEventSubscription(index: number): void; private updatePageEvents; addPageOnSuccessAction(eventIndex: number): void; removePageOnSuccessAction(eventIndex: number, actionIndex: number): void; updatePageOnSuccessAction(eventIndex: number, actionIndex: number, key: string, value: any): void; addPageOnErrorAction(eventIndex: number): void; removePageOnErrorAction(eventIndex: number, actionIndex: number): void; updatePageOnErrorAction(eventIndex: number, actionIndex: number, key: string, value: any): void; updatePageStateFormulaFn(index: number, fn: string): void; updatePageStateFormulaParam(index: number, param: keyof UpdateStateFormula, value: any): void; updatePageNestedStateFormulaFn(eventIndex: number, actionIndex: number, branch: 'on_success' | 'on_error', fn: string): void; updatePageNestedStateFormulaParam(eventIndex: number, actionIndex: number, branch: 'on_success' | 'on_error', param: keyof UpdateStateFormula, value: any): void; private _validationRules; readonly validationRules: _angular_core.Signal; private getDefaultErrorMessage; addValidationRule(): void; removeValidationRule(index: number): void; updateValidationRule(index: number, field: keyof ValidationRule, value: any): void; needsValue(type: string): boolean; private saveValidationRulesToComponent; trimAndFilter(v: string): boolean; static ɵfac: _angular_core.ɵɵFactoryDeclaration; static ɵcmp: _angular_core.ɵɵComponentDeclaration; } declare class PropertyEditorComponent implements OnInit { protected store: EruStudioStore; readonly schema: _angular_core.InputSignal; readonly value: _angular_core.InputSignal; readonly componentId: _angular_core.InputSignal; readonly disabled: _angular_core.InputSignal; readonly valueChange: _angular_core.OutputEmitterRef; protected autocompleteInputValue: _angular_core.WritableSignal; private lastRequestId; private lastRequestKey; readonly colorRangeFields: { key: 'color' | 'background'; label: string; }[]; readonly colorTokens: { label: string; value: string; }[]; /** Extract a `var(--studio-*)` token from a plain token value or a * color-mix(in srgb, N%, transparent) wrapper. Null if not a token. */ private extractToken; readonly isTokenColor: _angular_core.Signal; /** The token to preselect in the token dropdown ('' when custom). */ readonly tokenSelectValue: _angular_core.Signal; readonly hexColorValue: _angular_core.Signal; /** Current alpha as a 0–100 percentage (100 = fully opaque). */ readonly alphaValue: _angular_core.Signal; private parseColor; private toHex; /** Emit a color, using rgba() only when there is real transparency. */ private emitColor; /** Token color with optional opacity via color-mix (theme-reactive). */ private composeTokenColor; onColorHexChange(event: any): void; onOpacityChange(event: any): void; onColorTokenChange(tokenValue: string): void; readonly widthClass: _angular_core.Signal<"span-2" | "span-1">; readonly options: _angular_core.Signal; /** Request id for the saved-query list this editor's nested pickers need. */ private queryListRequestId; /** * Saved queries, for the Drill Query picker inside the dimensions editor. * * The `options` computed above only resolves the schema's OWN * `dynamic_options`; a list editor needs a second source for a field nested in * one of its rows. Usually free: the chart's own Query property has already * asked for the same list on this component. */ readonly queryOptions: _angular_core.Signal; /** * A stored drill query the fetched list does not contain — a renamed or deleted * query, or a list that has not arrived yet. Rendered as its own option so the * row never displays a value it is not actually holding. */ queryOptionMissing(value: string | undefined | null): boolean; filteredOptions: _angular_core.Signal; getDisplayValue(value: any): string; readonly selectedPreviewOption: _angular_core.Signal; trackByOption(index: number, option: SelectOption): string; /** * True when what has been typed matches no option, so the list can offer to * add it. A dynamic list is a suggestion, not a closed set — a query or field * being wired up may not exist on the server yet. */ readonly isNewAutocompleteValue: _angular_core.Signal; /** * Text shown for the stored value. Option lists are frequently * `{ value: 'cust_name', label: 'Customer Name' }`, so the input has to be * driven by this rather than by the raw value — bound straight to the value it * showed the stored key (an entity/field name) instead of the label the * dropdown had been showing. */ private syncAutocompleteDisplay; constructor(); ngOnInit(): void; getTextareaValue(): string; onInputChange(event: any): void; onNumberChange(event: any): void; onValueChange(value: any): void; /** Date properties are stored as yyyy-MM-dd — a plain, locale-independent * string the components can parse back without a timezone shift. */ onDateChange(event: any): void; onBooleanChange(event: any): void; onMultiselectChange(values: any[]): void; getMultiselectValue(): any[]; /** * Typing only filters the list — it does not commit. Emitting per keystroke * fought the display sync above, which rewrites the box to the matched * option's label; the half-typed text was replaced under the cursor. */ onAutocompleteInput(event: any): void; /** Commit free text on blur/Enter, mapping a typed label back to its value. */ onAutocompleteCommit(event: any): void; onAutocompleteSelection(event: any): void; getStatusOptions(): { label: string; color: string; [key: string]: any; }[]; addStatusOption(event: any): void; removeStatusOption(index: number): void; onStatusColorChange(index: number, event: any): void; /** * Tab titles, stored as one comma-separated string so pages authored before * this editor keep working. Every write goes back out in the same shape. */ getTabsList(): string[]; addTab(event: any): void; removeTab(index: number): void; moveTab(index: number, offset: number): void; /** * Fires on commit (blur / Enter), not per keystroke: the tabs component * reconciles its content containers against this list, so a half-typed title * must never reach it. */ onTabRename(index: number, event: any): void; private cleanTabLabel; private emitTabs; readonly menuItemKinds: SelectOption[]; readonly menuItemRoles: SelectOption[]; readonly toolbarItemKinds: SelectOption[]; /** Material 3 button appearances, the `matButton` input's own values. */ readonly itemButtonVariants: SelectOption[]; readonly itemColors: SelectOption[]; /** Deepest submenu an author can nest to. Material itself has no limit; this * is what stays readable in a one-column property panel. */ private readonly maxItemLevel; /** Kinds that hold nothing of their own — no label, icon or action. */ private readonly structuralItemKinds; getItemList(): MenuItemDef[]; isStructuralItem(item: MenuItemDef): boolean; /** A toolbar Menu, or anything nested under one, takes menu-item fields * rather than toolbar-item fields. */ isNestedItem(item: MenuItemDef): boolean; addItem(kind: string): void; removeItem(index: number): void; onItemChange(index: number, key: string, value: any): void; /** * Move an item together with everything nested under it, swapping with the * whole sibling block on that side. Swapping single rows would let a parent * cross into its own submenu. */ moveItem(index: number, offset: number): void; /** * Indent an item under the row above it (or pull it back out), carrying its * own nested entries with it so the shape of the submenu is preserved. */ indentItem(index: number, delta: number): void; canIndentItem(index: number, delta: number): boolean; /** * One deeper than the row above — a gap would orphan the item — and no * deeper than that row itself when it is a separator, which holds nothing. */ private maxIndentFor; /** The item at `index` plus every row nested under it. */ private itemBlockLength; getColorRanges(): ColorRange[]; /** Bounds come from the schema so a percentage property (progress) can pin * 0–100 while a general number property stays unconstrained. */ readonly rangeMin: _angular_core.Signal; readonly rangeMax: _angular_core.Signal; addColorRange(): void; removeColorRange(index: number): void; /** Blank clears the bound, which makes that side open-ended. */ onColorRangeChange(index: number, key: 'from' | 'to', event: any): void; private rangeColorValue; isRangeToken(index: number, field: 'color' | 'background'): boolean; rangeTokenValue(index: number, field: 'color' | 'background'): string; rangeHexValue(index: number, field: 'color' | 'background'): string; rangePreview(index: number, field: 'color' | 'background'): string; private emitRangeColor; onRangeTokenChange(index: number, field: 'color' | 'background', tokenValue: string): void; onRangeHexChange(index: number, field: 'color' | 'background', event: any): void; readonly aggFunctions: string[]; getPivotAggregations(): { name: string; aggregationFunction: string; label?: string; showSubtotals?: boolean; }[]; addPivotAggregation(): void; removePivotAggregation(index: number): void; onPivotAggChange(index: number, key: 'name' | 'aggregationFunction' | 'label' | 'showSubtotals', value: any): void; getDependentFields(): { def: string; dpef: string; }[]; addDependentField(): void; removeDependentField(index: number): void; onDependentFieldChange(index: number, key: 'def' | 'dpef', value: any): void; readonly chartAggFunctions: string[]; getChartDimensions(): any[]; addChartDimension(): void; removeChartDimension(index: number): void; onChartDimensionChange(index: number, key: string, value: any): void; /** * Entities for the colour-source picker. Read straight off the store — the data * model is already loaded there, so this needs no dynamic-data round trip. */ colorEntityOptions(): { value: string; label: string; }[]; /** * Fields of the chosen entity, the colour-bearing ones marked. * * Nothing is hidden — a field with no options today may gain them — but the * ones that actually carry colours are the point of the picker, so they say so. */ colorFieldOptions(entityName: string | undefined): { value: string; label: string; }[]; /** * Fields of an entity for the measure's format source. Numeric ones are marked: * a measure follows a number or currency column's decimals, grouping, symbol * and scale, and those are the fields that carry them. */ formatFieldOptions(entityName: string | undefined): { value: string; label: string; }[]; /** A stored colour that is a design token rather than a literal hex. */ isTokenColour(color: string | undefined): boolean; /** The hex to show in a swatch; tokens fall back to a neutral so it is editable. */ hexOfColour(color: string | undefined): string; /** * The chart-wide colour overrides, as chips. * * Same shape and same UI as a dimension's own overrides — a colour is picked, * not typed, at either level. */ getValueColors(): { value: string; color: string; }[]; addValueColor(event: any): void; removeValueColor(index: number): void; onValueColorChange(index: number, color: string): void; /** * A dimension's manual colour overrides as chips. * * Stored back as `{value, color}` rows rather than the old `Value=#hex` text: * a colour is picked, not typed. The parser still reads the text form, so * pages authored that way keep working. */ dimensionColors(dimension: any): { value: string; color: string; }[]; addDimensionColor(index: number, event: any): void; removeDimensionColor(index: number, colorIndex: number): void; onDimensionColorChange(index: number, colorIndex: number, color: string): void; getChartExtraOptions(): { path?: string; value?: any; }[]; addChartExtraOption(): void; removeChartExtraOption(index: number): void; onChartExtraOptionChange(index: number, key: 'path' | 'value', value: any): void; getChartMeasures(): any[]; addChartMeasure(): void; removeChartMeasure(index: number): void; onChartMeasureChange(index: number, key: string, value: any): void; /** * A list-valued property as an array. Pages saved through the JSON panel keep * it as a JSON string, so both forms have to read the same. */ private parseListValue; static ɵfac: _angular_core.ɵɵFactoryDeclaration; static ɵcmp: _angular_core.ɵɵComponentDeclaration; } /** * Picker for the `source:path` strings an action sends as its payload. * * A plain multi-select cannot express app state: app state has no declared * schema — a key exists only because an `update-state` action wrote it — so at * design time the key an author wants to send may not exist anywhere yet. This * accepts free text for that case and stores it as `app:`, which * `EruStudioStore.resolveApiPayloadFields` already resolves at runtime (and * drops when the key is absent). */ declare class PayloadFieldsPickerComponent { readonly value: _angular_core.InputSignal; readonly options: _angular_core.InputSignal; readonly label: _angular_core.InputSignal; readonly placeholder: _angular_core.InputSignal; readonly disabled: _angular_core.InputSignal; readonly valueChange: _angular_core.OutputEmitterRef; protected typed: _angular_core.WritableSignal; private readonly selected; /** * What has been typed, split into the alias being named and the text still * searching for a field. * * Authors write the rename the way it reads — `entity_id = ` and then pick * the variable — so once the `=` was typed the whole string was matched * against the options, nothing matched, and the list went empty exactly when * it was needed. The alias is held aside and only the remainder searches. * * Either order is accepted: the side carrying a `source:path` is the field, * the other is the alias. On selection it is stored field-first. */ private readonly parsedInput; /** Whether typed text names a listed variable — by its key or its label. */ private namesOption; /** Unselected known options, narrowed to what the author has typed. */ protected readonly filtered: _angular_core.Signal; /** * `app:` for typed text that matches no known option, so an app state * variable that does not exist yet can still be named. A typed `source:path` * is kept verbatim; a bare word is treated as an app state key, since state * and page keys are all listable at design time and app keys are not. */ protected readonly newField: _angular_core.Signal; /** Store the rename field-first, whichever way round it was typed. */ private withAlias; protected chipLabel(field: string): string; protected add(field: string | null): void; protected addTyped(event: MatChipInputEvent): void; protected remove(field: string): void; static ɵfac: _angular_core.ɵɵFactoryDeclaration; static ɵcmp: _angular_core.ɵɵComponentDeclaration; } declare class ComponentPaletteComponent { private store; private componentRegistry; readonly ui: _angular_core.Signal; readonly searchQuery: _angular_core.WritableSignal; readonly selectedTabIndex: _angular_core.Signal; readonly componentCategories: _angular_core.Signal<{ name: string; label: string; icon: string; components: eru_studio.ComponentDefinition[]; expanded: boolean; }[]>; onSearchChange(query: string): void; clearSearch(): void; onTabChange(event: any): void; private getCategoryLabel; private getCategoryIcon; onDragStart(event: DragEvent, component: any): void; onDragEnd(): void; static ɵfac: _angular_core.ɵɵFactoryDeclaration; static ɵcmp: _angular_core.ɵɵComponentDeclaration; } declare class RuntimeCanvasComponent implements OnInit, OnDestroy { store: EruStudioStore; /** * Parent canvas's store (if any). When this canvas is mounted *inside* * another canvas (e.g. via `nav_outlet`), its own store is fresh and * isolated — but `ds_page` requests from descendants need to reach the * host's request handler, which is wired to the OUTER store only. * * The constructor sets up a bridge: forward selected request types * (currently only `ds_page`) to the parent store, and copy responses * back into this store so the originating component's effect resolves. */ private parentStore; componentRegistry: ComponentRegistryService; private schemaService; private tailwindService; private formValidationService; private logicEvaluator; private readonly parentEnv; private readonly elementRef; /** * Breakpoint scope for everything this canvas renders, measured from the * canvas's own box. `window.innerWidth` is the wrong ruler whenever the * canvas is not full-width — a `nav_outlet` beside a sidebar, a widget host, * a page embedded in a narrow column — and it is the only reason such a page * laid itself out for the whole screen. */ private readonly breakpointScope; private stopObservingWidth; readonly dynamicComponentInjector: EnvironmentInjector; readonly componentsReady: _angular_core.WritableSignal; readonly components: _angular_core.Signal; protected pageId: _angular_core.Signal; readonly initialEntityId: _angular_core.WritableSignal; readonly targetPageId: _angular_core.WritableSignal; readonly inputPageDef: _angular_core.WritableSignal; set initialEntityIdSetter(value: string); /** * If set, the canvas fires a `ds_page` request on **its own store** * and calls `setCurrentPage` once a response lands. This only works * if something is watching that store's dynamic-data queue — which * is true for the host's primary canvas (processo wires this up), * but NOT for inner canvas instances mounted inside `nav_outlet` * (each gets a fresh store nobody watches). * * For inner mounts, prefer `pageDef` — the parent fetches the def * via its own already-watched store and hands the resolved object * here. */ set targetPageIdSetter(value: string); /** * Pre-fetched page definition. When set, the canvas calls * `setCurrentPage(pageDef)` directly — no fetch needed. This is * how `nav_outlet` mounts a page: it fetches via the outer store * (which processo watches) and passes the resolved def in. */ set pageDefSetter(value: EruPage | null); /** * Keep this canvas's page state when it is unmounted, so returning to the * page (Back, or navigating to it again) resumes instead of starting blank. * Set by `nav_outlet`'s `retain_page_state` property. The snapshot is parked * on the root store, which survives the swap that destroys this canvas. */ set retainPageStateSetter(value: boolean); private readonly pageDataIdentity; private readonly lastPageDataRequestId; /** Page the in-flight ds_page_data fetch was issued for. A navigation can * land before the response does, and the record must not be applied to * whatever page happens to be current by then. */ private readonly pageDataRequestPageId; private readonly lastPageDefRequestId; private firedOnLoadPageId; constructor(); /** * Nested-canvas requests are relayed to the parent by the STORE itself * (EruStudioStore.requestDynamicData), which forwards to the parent under one * id, records the request locally for its own bookkeeping, merges the * parent's responses back into `dynamicDataResponses`, and clears both copies * on removal. * * This component used to bridge the same requests a second time * (`installParentBridge`), walking its store's pending requests and calling * `parent.requestDynamicData` again. Its dedupe map was separate from the * store's, so it could not see the record the store had just relayed and * forwarded it under a NEW parent id — the host then ran the same query * twice. That was the double `executegroup`, and the doubled per-group * `execute` calls behind it, on every page mounted in a nav_outlet shell. */ ngOnInit(): void; ngOnDestroy(): void; onWindowResize(): void; private updateBreakpointFromViewport; getComponentClass(type: string): any; isContainer(type: string): boolean; getPropertyValue(component: EruComponent, key: string, defaultValue?: any): any; getDataKey(component: EruComponent): string; getChildStyles(component: EruComponent): any; isChildVisible(component: EruComponent): boolean; getZIndex(level: number): number; static ɵfac: _angular_core.ɵɵFactoryDeclaration; static ɵcmp: _angular_core.ɵɵComponentDeclaration; } declare class GhostComponent extends BaseComponent { protected readonly lines: _angular_core.Signal; protected getComponentPropertySchema(): PropertySchema[]; protected getComponentDefaultProperties(): EruComponentProperties; protected getComponentDefaultStyles(): StyleProperties; static ɵfac: _angular_core.ɵɵFactoryDeclaration; static ɵcmp: _angular_core.ɵɵComponentDeclaration; } declare class PageStatePanelComponent { protected store: EruStudioStore; readonly currentPage: _angular_core.Signal; readonly stateVars: _angular_core.Signal; readonly computedState: _angular_core.Signal>; /** * Declaring, editing and deleting variables is authoring, so it belongs to * edit mode. The panel itself opens in view mode as well — watching runtime * values is exactly what it is for there — and simply shows them read-only. */ readonly canEdit: _angular_core.Signal; /** * Which state this panel is showing. App state had no viewer at all — it is * written by `update-state` with `state_scope: 'app'` and read as * `@app.`, so the only way to see what was in it was the sessionStorage * blob in devtools. */ readonly scope: _angular_core.WritableSignal<"page" | "app">; constructor(); setScope(scope: 'page' | 'app'): void; /** * App state as a sorted key/value list. * * Read-only by design: unlike page state there is nothing to declare — the * keys exist only because an action wrote them, so there is no schema to edit * and an inline change here would be overwritten by the next write. It is * window-wide and shared across every page in the process, which is exactly * why seeing it matters when a value is not what a page expected. */ readonly appStateVars: _angular_core.Signal<{ key: string; value: any; }[]>; /** * Render a state value for display. Objects and arrays are the interesting * case — a payload parked in state showed as "[object Object]", which tells * the reader nothing about why their expression did not match. */ formatValue(value: any): string; /** * Text put on the clipboard for a state value. Objects and arrays are * pretty-printed rather than using the one-line `formatValue` form — what * gets copied is usually pasted somewhere it needs to be read. */ clipboardValue(value: any): string; readonly copiedKey: _angular_core.WritableSignal; copyValue(scope: string, key: string, value: any): void; private fallbackCopy; readonly systemStateVars: _angular_core.Signal<{ key: string; value: any; }[]>; readonly editingIndex: _angular_core.WritableSignal; readonly editBuffer: _angular_core.WritableSignal; addStateVar(): void; removeStateVar(index: number): void; startEdit(index: number, sv: PageStateVariable): void; cancelEdit(): void; saveEdit(): void; updateBuffer(field: keyof PageStateVariable, value: any): void; updateFormulaFn(fn: string): void; getFilterExpr(): string; setFilterExpr(value: string): void; updateFormulaField(param: keyof StateFormula, value: any): void; clearFormula(): void; /** * Arrays a formula can aggregate over besides the page's own records — every * other declared state variable, offered as `state:`. This is what makes * a grid's checkbox selection summable: the grid writes `selected_rows` into * state and the formula reads it back from there. */ readonly formulaSourceStateVars: _angular_core.Signal<{ value: string; label: string; }[]>; /** * The `state:[.]` source split into its two authored halves — the * dropdown picks the variable, the text field drills into it. */ readonly formulaSourceKey: _angular_core.Signal; readonly formulaSourcePath: _angular_core.Signal; updateFormulaSourceKey(key: string): void; updateFormulaSourcePath(path: string): void; readonly formulaFns: { value: string; label: string; }[]; static ɵfac: _angular_core.ɵɵFactoryDeclaration; static ɵcmp: _angular_core.ɵɵComponentDeclaration; } declare class CanvasEventHandlerDirective { private el; private store; constructor(el: ElementRef); onMouseEnter(event: MouseEvent): void; onMouseLeave(event: MouseEvent): void; onMouseDown(event: MouseEvent): void; onMouseUp(event: MouseEvent): void; onClick(event: MouseEvent): void; onFocus(event: FocusEvent): void; onBlur(event: FocusEvent): void; onInput(event: Event): void; private getDistanceToTarget; static ɵfac: _angular_core.ɵɵFactoryDeclaration; static ɵdir: _angular_core.ɵɵDirectiveDeclaration; } declare const APP_CONSTANTS: { readonly APP_NAME: "ERU Studio"; readonly APP_VERSION: "3.0.0"; readonly API_BASE_URL: "/api/eru"; readonly STORAGE_KEYS: { readonly PROJECTS: "eru-studio-projects"; readonly CURRENT_PROJECT: "eru-studio-current-project"; readonly USER_PREFERENCES: "eru-studio-preferences"; readonly PAGE_DATA: "eru-studio-page-data"; }; readonly DESIGNER: { readonly DEFAULT_BREAKPOINT: TailwindBreakpoint | "base"; readonly DEFAULT_ZOOM: 1; readonly GRID_SIZE: 8; readonly SNAP_THRESHOLD: 5; readonly MIN_ZOOM: 0.25; readonly MAX_ZOOM: 3; readonly HISTORY_LIMIT: 50; }; readonly COMPONENT: { readonly MAX_NESTING_DEPTH: 10; readonly MAX_CHILDREN_PER_CONTAINER: 100; readonly MIN_WIDTH: 20; readonly MIN_HEIGHT: 20; }; readonly UPLOAD: { readonly MAX_FILE_SIZE: number; readonly ALLOWED_IMAGE_TYPES: readonly ["image/jpeg", "image/png", "image/gif", "image/webp"]; readonly ALLOWED_DOCUMENT_TYPES: readonly ["application/pdf"]; }; readonly BREAKPOINTS: { readonly sm: 640; readonly md: 768; readonly lg: 1024; readonly xl: 1280; readonly '2xl': 1536; }; }; declare const DRAG_DROP_TYPES: { readonly COMPONENT: "application/eru-component"; readonly REORDER: "application/eru-reorder"; }; declare const KEYBOARD_SHORTCUTS: { readonly SAVE: "cmd+s"; readonly UNDO: "cmd+z"; readonly REDO: "cmd+shift+z"; readonly DELETE: "delete"; readonly COPY: "cmd+c"; readonly PASTE: "cmd+v"; readonly SELECT_ALL: "cmd+a"; readonly ZOOM_IN: "cmd+="; readonly ZOOM_OUT: "cmd+-"; readonly ZOOM_FIT: "cmd+0"; }; /** * Hand the browser a file that arrived as a base64 string. * * The case this exists for: an API that returns a document as a JSON * attribute instead of streaming it, so there is no URL to point the browser * at. The bytes are decoded into a Blob and saved through a synthetic anchor * click — the same route the attachment component uses to open a file, with a * `download` attribute instead of a new tab. */ /** Resolve an author-supplied file type — a MIME type or a bare extension. */ declare function resolveFileMimeType(fileType?: string | null): string; /** * The extension a name should end with for this MIME type, or '' when the type * is one we have no mapping for — better a name with no extension than a made * up one. */ declare function extensionForMimeType(mimeType: string): string; /** * Strip a `data:;base64,` prefix and any whitespace a JSON response may * have wrapped the payload in. Returns the MIME type the prefix declared, so a * response that carries its own type needs nothing configured. */ declare function parseBase64Payload(value: string): { base64: string; mime_type: string; }; /** * Save a base64 string as a file. * * `fileType` is a MIME type or a bare extension; a `data:` prefix on the value * supplies it when nothing is configured. Returns false when there is nothing * to download or the value is not decodable, so the caller can report it * rather than leave the click looking successful. */ declare function downloadBase64File(value: string, fileName?: string | null, fileType?: string | null): boolean; export { ALL_COMPONENTS, APP_CONSTANTS, AssetsPanelComponent, AutocompleteComponent, BASIC_COMPONENTS, BadgeComponent, BarChartComponent, BaseComponent, BreakpointScopeService, ButtonComponent, ButtonToggleComponent, CHART_AGGREGATIONS, CHART_OTHERS_LABEL, CanvasComponent, CanvasEventHandlerDirective, CardComponent, ChartBaseComponent, ChartSelectorsComponent, ChipsComponent, ComponentPaletteComponent, ComponentRegistryService, ComponentSchemaService, DATA_COMPONENTS, DRAG_DROP_TYPES, DividerComponent, ERU_COMPONENTS, EruApiService, EruAppRouteService, EruPageComponent, EruStudioStore, ExpansionPanelComponent, FORM_COMPONENTS, FlexContainerComponent, FloatingPanelComponent, FormValidationService, GhostComponent, GridComponent, GridContainerComponent, GridListComponent, IconComponent, ImageComponent, KEYBOARD_SHORTCUTS, LAYOUT_COMPONENTS, LOADING_COMPONENTS, LayerTreeItemComponent, LayersPanelComponent, LineChartComponent, ListComponent, MenuComponent, NAVIGATION_COMPONENTS, NAV_PARAM_JSON_PREFIX, NAV_PARAM_MAX_LENGTH, NavMenuComponent, NavOutletComponent, PageRefComponent, PageStatePanelComponent, PayloadFieldsPickerComponent, PhoneComponent, PieChartComponent, ProgressBarComponent, ProgressSpinnerComponent, PropertiesPanelComponent, PropertyEditorComponent, PropertyToTailwindService, RadioComponent, RowDataContext, RuntimeCanvasComponent, SidebarStepperComponent, SidenavComponent, SlideToggleComponent, SliderComponent, StepperComponent, TabsComponent, TextComponent, TileComponent, TimerComponent, ToolbarComponent, TreeComponent, WidgetComponent, aggregateChartData, applyChartOptionOverrides, applyPagePatch, buildStagingTree, chartCategoryValue, chartDimensionLabel, chartLabelFormatter, chartMeasureLabel, downloadBase64File, entityFieldValueColors, extensionForMimeType, isValidWebsiteUrl, parseBase64Payload, parseChartDimensions, parseChartMeasures, parseChartOptionOverrides, parseChartValueColors, parsePositiveInt, reduceRows, resolveChartValue, resolveFileMimeType, stripMountedPages, stripRuntimeEventKeys }; export type { ChartAggregateOptions, ChartAggregation, ChartDataset, ChartDimension, ChartDrillStep, ChartGroup, ChartMeasure, ChartOptionOverride, ChartSeries, ChartSort, ChartValueColor, ColorRange, ComponentCategory, ComponentCondition, ComponentDefinition, ComponentEvent, ComponentEventSubscription, ComponentPosition, ComponentStyles, CustomValidator, DesignerState, DragState, DynamicDataRequest, Entity, EntityField, EruComponent, EruComponentProperties, EruPage, EventSchema, EventSubscription, FormControlInfo, HistoryState, MasterDetailConfig, NavParam, PageContainerConfig, PageData, PageNavigationEvent, PagePatch, PagePatchResult, PageStatePatch, PageStateVariable, PageStyles, PropertyCategory, PropertySchema, PropertyType, RecordFieldPatch, ResponsiveClasses, ResponsiveEruComponentProperties, ResponsiveStyleProperties, RetainedPageState, ScannedPageComponent, SelectOption, ServerValidationNotice, StateFilter, StateFormula, StyleProperties, StyleSchema, TailwindBreakpoint, UIState, UpdateStateFormula, ValidationRule, ViewportState, VisibilityRule };