@memberjunction/ng-search 5.29.0 → 5.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -297,7 +297,7 @@ export class SearchFilterComponent {
297
297
  this.MinScoreChanged.emit(this.MinScorePercent);
298
298
  }
299
299
  static ɵfac = function SearchFilterComponent_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || SearchFilterComponent)(); };
300
- static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: SearchFilterComponent, selectors: [["mj-search-filter"]], inputs: { Filters: "Filters", ActiveFilters: "ActiveFilters", Collapsible: "Collapsible", ShowRelevanceSlider: "ShowRelevanceSlider", MinScorePercent: "MinScorePercent", ServerMinScorePercent: "ServerMinScorePercent" }, outputs: { FilterChanged: "FilterChanged", FiltersCleared: "FiltersCleared", CloseRequested: "CloseRequested", MinScoreChanged: "MinScoreChanged" }, standalone: false, decls: 13, vars: 3, consts: [[1, "filter-panel"], [1, "filter-panel-header"], [1, "filter-panel-title"], [1, "fa-solid", "fa-filter"], [1, "filter-header-actions"], [1, "filter-clear-all"], ["title", "Hide filters", 1, "filter-close-btn", 3, "click"], [1, "fa-solid", "fa-times"], [1, "filter-category"], [1, "filter-empty"], [1, "filter-clear-all", 3, "click"], [1, "filter-category-header-row"], [1, "filter-category-header", 3, "click"], [1, "filter-category-name"], [1, "fa-solid", "filter-category-chevron", 3, "fa-chevron-down", "fa-chevron-up"], [1, "filter-select-all", 3, "title"], [1, "filter-options"], [1, "fa-solid", "filter-category-chevron"], [1, "filter-select-all", 3, "click", "title"], [1, "filter-option", "filter-option-readonly"], [1, "filter-option"], [3, "class"], [1, "filter-option-label"], [1, "filter-option-count"], [1, "filter-option", 3, "click"], [1, "filter-checkbox"], [1, "fa-solid", "fa-check"], [1, "filter-category-header"], [1, "filter-relevance-slider"], ["type", "range", "min", "0", "max", "100", "step", "5", 1, "relevance-range", 3, "input", "mouseup", "touchend", "value"], [1, "relevance-value"], [1, "filter-server-threshold"], [1, "server-threshold-label"], [1, "server-threshold-warning"], [1, "fa-solid", "fa-arrow-rotate-right"]], template: function SearchFilterComponent_Template(rf, ctx) { if (rf & 1) {
300
+ static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: SearchFilterComponent, selectors: [["mj-search-filter"]], inputs: { Filters: "Filters", ActiveFilters: "ActiveFilters", Collapsible: "Collapsible", ShowRelevanceSlider: "ShowRelevanceSlider", MinScorePercent: "MinScorePercent", ServerMinScorePercent: "ServerMinScorePercent" }, outputs: { FilterChanged: "FilterChanged", FiltersCleared: "FiltersCleared", CloseRequested: "CloseRequested", MinScoreChanged: "MinScoreChanged" }, standalone: false, decls: 13, vars: 3, consts: [[1, "filter-panel"], [1, "filter-panel-header"], [1, "filter-panel-title"], [1, "fa-solid", "fa-filter"], [1, "filter-header-actions"], [1, "filter-clear-all"], ["title", "Hide filters", "aria-label", "Hide filters", 1, "filter-close-btn", 3, "click"], [1, "fa-solid", "fa-times"], [1, "filter-category"], [1, "filter-empty"], [1, "filter-clear-all", 3, "click"], [1, "filter-category-header-row"], [1, "filter-category-header", 3, "click"], [1, "filter-category-name"], [1, "fa-solid", "filter-category-chevron", 3, "fa-chevron-down", "fa-chevron-up"], [1, "filter-select-all", 3, "title"], [1, "filter-options"], [1, "fa-solid", "filter-category-chevron"], [1, "filter-select-all", 3, "click", "title"], [1, "filter-option", "filter-option-readonly"], [1, "filter-option"], [3, "class"], [1, "filter-option-label"], [1, "filter-option-count"], [1, "filter-option", 3, "click"], [1, "filter-checkbox"], [1, "fa-solid", "fa-check"], [1, "filter-category-header"], [1, "filter-relevance-slider"], ["type", "range", "min", "0", "max", "100", "step", "5", 1, "relevance-range", 3, "input", "mouseup", "touchend", "value"], [1, "relevance-value"], [1, "filter-server-threshold"], [1, "server-threshold-label"], [1, "server-threshold-warning"], [1, "fa-solid", "fa-arrow-rotate-right"]], template: function SearchFilterComponent_Template(rf, ctx) { if (rf & 1) {
301
301
  i0.ɵɵelementStart(0, "div", 0)(1, "div", 1)(2, "span", 2);
302
302
  i0.ɵɵelement(3, "i", 3);
303
303
  i0.ɵɵtext(4, " Filters ");
@@ -325,7 +325,7 @@ export class SearchFilterComponent {
325
325
  }
326
326
  (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(SearchFilterComponent, [{
327
327
  type: Component,
328
- args: [{ standalone: false, selector: 'mj-search-filter', template: "<div class=\"filter-panel\">\n <div class=\"filter-panel-header\">\n <span class=\"filter-panel-title\">\n <i class=\"fa-solid fa-filter\"></i> Filters\n </span>\n <div class=\"filter-header-actions\">\n @if (HasActiveFilters()) {\n <button class=\"filter-clear-all\" (click)=\"ClearAll()\">\n Clear ({{ GetActiveFilterCount() }})\n </button>\n }\n <button class=\"filter-close-btn\" (click)=\"CloseRequested.emit()\" title=\"Hide filters\">\n <i class=\"fa-solid fa-times\"></i>\n </button>\n </div>\n </div>\n\n @for (filter of Filters; track filter.Category) {\n <div class=\"filter-category\">\n <div class=\"filter-category-header-row\">\n <div\n class=\"filter-category-header\"\n (click)=\"ToggleCategory(filter.Category)\"\n [class.filter-category-collapsed]=\"IsCategoryCollapsed(filter.Category)\"\n >\n <span class=\"filter-category-name\">{{ filter.Category }}</span>\n @if (Collapsible) {\n <i class=\"fa-solid filter-category-chevron\"\n [class.fa-chevron-down]=\"IsCategoryCollapsed(filter.Category)\"\n [class.fa-chevron-up]=\"!IsCategoryCollapsed(filter.Category)\"></i>\n }\n </div>\n @if (!IsCategoryCollapsed(filter.Category) && filter.Options.length > 1) {\n <button class=\"filter-select-all\"\n (click)=\"SelectAll(filter.Category); $event.stopPropagation()\"\n [title]=\"IsAllSelected(filter.Category) ? 'Clear all' : 'Select all'\">\n {{ IsAllSelected(filter.Category) ? 'Clear' : 'All' }}\n </button>\n }\n </div>\n\n @if (!IsCategoryCollapsed(filter.Category)) {\n <div class=\"filter-options\">\n @for (option of filter.Options; track option.Value) {\n @if (filter.Options.length === 1) {\n <!-- Single option: read-only label (no checkbox, not interactive) -->\n <div class=\"filter-option filter-option-readonly\">\n @if (option.Icon) {\n <i [class]=\"option.Icon + ' filter-option-icon'\"></i>\n }\n <span class=\"filter-option-label\">{{ option.Label }}</span>\n <span class=\"filter-option-count\">{{ option.Count }}</span>\n </div>\n } @else {\n <label class=\"filter-option\" (click)=\"ToggleOption(filter.Category, option.Value); $event.preventDefault()\">\n <span class=\"filter-checkbox\" [class.filter-checkbox-checked]=\"IsOptionSelected(filter.Category, option.Value)\">\n @if (IsOptionSelected(filter.Category, option.Value)) {\n <i class=\"fa-solid fa-check\"></i>\n }\n </span>\n @if (option.Icon) {\n <i [class]=\"option.Icon + ' filter-option-icon'\"></i>\n }\n <span class=\"filter-option-label\">{{ option.Label }}</span>\n <span class=\"filter-option-count\">{{ option.Count }}</span>\n </label>\n }\n }\n </div>\n }\n </div>\n }\n\n <!-- Relevance Threshold Slider -->\n @if (ShowRelevanceSlider) {\n <div class=\"filter-category\">\n <div class=\"filter-category-header-row\">\n <div class=\"filter-category-header\">\n <span class=\"filter-category-name\">Min Relevance</span>\n </div>\n </div>\n <div class=\"filter-relevance-slider\">\n <input\n type=\"range\"\n min=\"0\" max=\"100\" step=\"5\"\n [value]=\"MinScorePercent\"\n (input)=\"OnMinScorePreview($any($event.target).value)\"\n (mouseup)=\"OnMinScoreCommit()\"\n (touchend)=\"OnMinScoreCommit()\"\n class=\"relevance-range\" />\n <span class=\"relevance-value\">{{ MinScorePercent }}%</span>\n </div>\n @if (ServerMinScorePercent > 0) {\n <div class=\"filter-server-threshold\">\n <span class=\"server-threshold-label\">Server threshold: {{ ServerMinScorePercent }}%</span>\n @if (MinScorePercent < ServerMinScorePercent) {\n <span class=\"server-threshold-warning\">\n <i class=\"fa-solid fa-arrow-rotate-right\"></i> Will re-query\n </span>\n }\n </div>\n }\n </div>\n }\n\n @if (Filters.length === 0 && !ShowRelevanceSlider) {\n <div class=\"filter-empty\">\n <p>No filters available</p>\n </div>\n }\n</div>\n", styles: ["/* Search Filter Panel */\n\n.filter-panel {\n border: 1px solid var(--mj-border-default);\n border-radius: 8px;\n background: var(--mj-bg-surface);\n overflow: hidden;\n}\n\n.filter-panel-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 0.75rem 1rem;\n border-bottom: 1px solid var(--mj-border-subtle);\n}\n\n.filter-panel-title {\n font-weight: 600;\n font-size: 0.85rem;\n color: var(--mj-text-primary);\n display: flex;\n align-items: center;\n gap: 0.5rem;\n}\n\n.filter-clear-all {\n border: none;\n background: none;\n color: var(--mj-brand-primary);\n font-size: 0.78rem;\n cursor: pointer;\n padding: 0.15rem 0.4rem;\n border-radius: 4px;\n}\n\n.filter-clear-all:hover {\n background: color-mix(in srgb, var(--mj-brand-primary) 10%, var(--mj-bg-surface));\n}\n\n.filter-header-actions {\n display: flex;\n align-items: center;\n gap: 6px;\n}\n\n.filter-close-btn {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 24px;\n height: 24px;\n border: none;\n border-radius: 4px;\n background: none;\n color: var(--mj-text-muted);\n font-size: 0.75rem;\n cursor: pointer;\n transition: all 0.15s;\n}\n\n.filter-close-btn:hover {\n background: var(--mj-bg-surface-hover);\n color: var(--mj-text-primary);\n}\n\n/* Category */\n.filter-category {\n border-bottom: 1px solid var(--mj-border-subtle);\n}\n\n.filter-category:last-child {\n border-bottom: none;\n}\n\n.filter-category-header-row {\n display: flex;\n align-items: center;\n}\n\n.filter-category-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 0.6rem 1rem;\n cursor: pointer;\n transition: background 0.1s ease;\n flex: 1;\n}\n\n.filter-category-header:hover {\n background: var(--mj-bg-surface-hover);\n}\n\n.filter-select-all {\n padding: 2px 8px;\n border: 1px solid var(--mj-border-default);\n border-radius: 4px;\n background: none;\n color: var(--mj-text-muted);\n font-size: 0.65rem;\n font-weight: 500;\n cursor: pointer;\n margin-right: 8px;\n transition: color 0.15s ease, border-color 0.15s ease;\n}\n\n.filter-select-all:hover {\n color: var(--mj-brand-primary);\n border-color: var(--mj-brand-primary);\n}\n\n.filter-category-name {\n font-weight: 500;\n font-size: 0.82rem;\n color: var(--mj-text-primary);\n}\n\n.filter-category-chevron {\n font-size: 0.65rem;\n color: var(--mj-text-muted);\n}\n\n/* Options */\n.filter-options {\n padding: 0.25rem 0;\n}\n\n.filter-option {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n padding: 0.4rem 1rem;\n cursor: pointer;\n transition: background 0.1s ease;\n font-size: 0.82rem;\n}\n\n.filter-option:hover {\n background: var(--mj-bg-surface-hover);\n}\n\n.filter-option-readonly {\n cursor: default;\n opacity: 0.85;\n}\n.filter-option-readonly:hover {\n background: transparent;\n}\n\n.filter-checkbox {\n width: 1rem;\n height: 1rem;\n border: 1.5px solid var(--mj-border-strong);\n border-radius: 3px;\n display: flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n font-size: 0.6rem;\n transition: all 0.15s ease;\n}\n\n.filter-checkbox-checked {\n background: var(--mj-brand-primary);\n border-color: var(--mj-brand-primary);\n color: var(--mj-text-inverse);\n}\n\n.filter-option-icon {\n color: var(--mj-text-muted);\n font-size: 0.78rem;\n width: 1rem;\n text-align: center;\n}\n\n.filter-option-label {\n flex: 1;\n color: var(--mj-text-secondary);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.filter-option-count {\n color: var(--mj-text-muted);\n font-size: 0.72rem;\n flex-shrink: 0;\n}\n\n/* Empty state */\n.filter-empty {\n padding: 1.5rem 1rem;\n text-align: center;\n color: var(--mj-text-muted);\n font-size: 0.82rem;\n}\n\n/* Relevance Slider */\n.filter-relevance-slider {\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 8px 12px;\n}\n.relevance-range {\n flex: 1;\n height: 4px;\n -webkit-appearance: none;\n appearance: none;\n background: var(--mj-border-default);\n border-radius: 2px;\n outline: none;\n cursor: pointer;\n}\n.relevance-range::-webkit-slider-thumb {\n -webkit-appearance: none;\n width: 16px;\n height: 16px;\n border-radius: 50%;\n background: var(--mj-brand-primary);\n cursor: pointer;\n border: 2px solid var(--mj-bg-surface);\n box-shadow: 0 1px 3px rgba(0,0,0,0.2);\n}\n.relevance-range::-moz-range-thumb {\n width: 16px;\n height: 16px;\n border-radius: 50%;\n background: var(--mj-brand-primary);\n cursor: pointer;\n border: 2px solid var(--mj-bg-surface);\n box-shadow: 0 1px 3px rgba(0,0,0,0.2);\n}\n.relevance-value {\n font-size: 12px;\n font-weight: 600;\n color: var(--mj-brand-primary);\n min-width: 36px;\n text-align: right;\n}\n\n/* Server threshold indicator */\n.filter-server-threshold {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 2px 12px 6px;\n font-size: 10px;\n color: var(--mj-text-muted);\n}\n.server-threshold-warning {\n color: var(--mj-status-warning-text, var(--mj-status-warning));\n font-weight: 600;\n}\n"] }]
328
+ args: [{ standalone: false, selector: 'mj-search-filter', template: "<div class=\"filter-panel\">\n <div class=\"filter-panel-header\">\n <span class=\"filter-panel-title\">\n <i class=\"fa-solid fa-filter\"></i> Filters\n </span>\n <div class=\"filter-header-actions\">\n @if (HasActiveFilters()) {\n <button class=\"filter-clear-all\" (click)=\"ClearAll()\">\n Clear ({{ GetActiveFilterCount() }})\n </button>\n }\n <button class=\"filter-close-btn\" (click)=\"CloseRequested.emit()\" title=\"Hide filters\" aria-label=\"Hide filters\">\n <i class=\"fa-solid fa-times\"></i>\n </button>\n </div>\n </div>\n\n @for (filter of Filters; track filter.Category) {\n <div class=\"filter-category\">\n <div class=\"filter-category-header-row\">\n <div\n class=\"filter-category-header\"\n (click)=\"ToggleCategory(filter.Category)\"\n [class.filter-category-collapsed]=\"IsCategoryCollapsed(filter.Category)\"\n >\n <span class=\"filter-category-name\">{{ filter.Category }}</span>\n @if (Collapsible) {\n <i class=\"fa-solid filter-category-chevron\"\n [class.fa-chevron-down]=\"IsCategoryCollapsed(filter.Category)\"\n [class.fa-chevron-up]=\"!IsCategoryCollapsed(filter.Category)\"></i>\n }\n </div>\n @if (!IsCategoryCollapsed(filter.Category) && filter.Options.length > 1) {\n <button class=\"filter-select-all\"\n (click)=\"SelectAll(filter.Category); $event.stopPropagation()\"\n [title]=\"IsAllSelected(filter.Category) ? 'Clear all' : 'Select all'\">\n {{ IsAllSelected(filter.Category) ? 'Clear' : 'All' }}\n </button>\n }\n </div>\n\n @if (!IsCategoryCollapsed(filter.Category)) {\n <div class=\"filter-options\">\n @for (option of filter.Options; track option.Value) {\n @if (filter.Options.length === 1) {\n <!-- Single option: read-only label (no checkbox, not interactive) -->\n <div class=\"filter-option filter-option-readonly\">\n @if (option.Icon) {\n <i [class]=\"option.Icon + ' filter-option-icon'\"></i>\n }\n <span class=\"filter-option-label\">{{ option.Label }}</span>\n <span class=\"filter-option-count\">{{ option.Count }}</span>\n </div>\n } @else {\n <label class=\"filter-option\" (click)=\"ToggleOption(filter.Category, option.Value); $event.preventDefault()\">\n <span class=\"filter-checkbox\" [class.filter-checkbox-checked]=\"IsOptionSelected(filter.Category, option.Value)\">\n @if (IsOptionSelected(filter.Category, option.Value)) {\n <i class=\"fa-solid fa-check\"></i>\n }\n </span>\n @if (option.Icon) {\n <i [class]=\"option.Icon + ' filter-option-icon'\"></i>\n }\n <span class=\"filter-option-label\">{{ option.Label }}</span>\n <span class=\"filter-option-count\">{{ option.Count }}</span>\n </label>\n }\n }\n </div>\n }\n </div>\n }\n\n <!-- Relevance Threshold Slider -->\n @if (ShowRelevanceSlider) {\n <div class=\"filter-category\">\n <div class=\"filter-category-header-row\">\n <div class=\"filter-category-header\">\n <span class=\"filter-category-name\">Min Relevance</span>\n </div>\n </div>\n <div class=\"filter-relevance-slider\">\n <input\n type=\"range\"\n min=\"0\" max=\"100\" step=\"5\"\n [value]=\"MinScorePercent\"\n (input)=\"OnMinScorePreview($any($event.target).value)\"\n (mouseup)=\"OnMinScoreCommit()\"\n (touchend)=\"OnMinScoreCommit()\"\n class=\"relevance-range\" />\n <span class=\"relevance-value\">{{ MinScorePercent }}%</span>\n </div>\n @if (ServerMinScorePercent > 0) {\n <div class=\"filter-server-threshold\">\n <span class=\"server-threshold-label\">Server threshold: {{ ServerMinScorePercent }}%</span>\n @if (MinScorePercent < ServerMinScorePercent) {\n <span class=\"server-threshold-warning\">\n <i class=\"fa-solid fa-arrow-rotate-right\"></i> Will re-query\n </span>\n }\n </div>\n }\n </div>\n }\n\n @if (Filters.length === 0 && !ShowRelevanceSlider) {\n <div class=\"filter-empty\">\n <p>No filters available</p>\n </div>\n }\n</div>\n", styles: ["/* Search Filter Panel */\n\n.filter-panel {\n border: 1px solid var(--mj-border-default);\n border-radius: 8px;\n background: var(--mj-bg-surface);\n overflow: hidden;\n}\n\n.filter-panel-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 0.75rem 1rem;\n border-bottom: 1px solid var(--mj-border-subtle);\n}\n\n.filter-panel-title {\n font-weight: 600;\n font-size: 0.85rem;\n color: var(--mj-text-primary);\n display: flex;\n align-items: center;\n gap: 0.5rem;\n}\n\n.filter-clear-all {\n border: none;\n background: none;\n color: var(--mj-brand-primary);\n font-size: 0.78rem;\n cursor: pointer;\n padding: 0.15rem 0.4rem;\n border-radius: 4px;\n}\n\n.filter-clear-all:hover {\n background: color-mix(in srgb, var(--mj-brand-primary) 10%, var(--mj-bg-surface));\n}\n\n.filter-header-actions {\n display: flex;\n align-items: center;\n gap: 6px;\n}\n\n.filter-close-btn {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 24px;\n height: 24px;\n border: none;\n border-radius: 4px;\n background: none;\n color: var(--mj-text-muted);\n font-size: 0.75rem;\n cursor: pointer;\n transition: all 0.15s;\n}\n\n.filter-close-btn:hover {\n background: var(--mj-bg-surface-hover);\n color: var(--mj-text-primary);\n}\n\n/* Category */\n.filter-category {\n border-bottom: 1px solid var(--mj-border-subtle);\n}\n\n.filter-category:last-child {\n border-bottom: none;\n}\n\n.filter-category-header-row {\n display: flex;\n align-items: center;\n}\n\n.filter-category-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 0.6rem 1rem;\n cursor: pointer;\n transition: background 0.1s ease;\n flex: 1;\n}\n\n.filter-category-header:hover {\n background: var(--mj-bg-surface-hover);\n}\n\n.filter-select-all {\n padding: 2px 8px;\n border: 1px solid var(--mj-border-default);\n border-radius: 4px;\n background: none;\n color: var(--mj-text-muted);\n font-size: 0.65rem;\n font-weight: 500;\n cursor: pointer;\n margin-right: 8px;\n transition: color 0.15s ease, border-color 0.15s ease;\n}\n\n.filter-select-all:hover {\n color: var(--mj-brand-primary);\n border-color: var(--mj-brand-primary);\n}\n\n.filter-category-name {\n font-weight: 500;\n font-size: 0.82rem;\n color: var(--mj-text-primary);\n}\n\n.filter-category-chevron {\n font-size: 0.65rem;\n color: var(--mj-text-muted);\n}\n\n/* Options */\n.filter-options {\n padding: 0.25rem 0;\n}\n\n.filter-option {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n padding: 0.4rem 1rem;\n cursor: pointer;\n transition: background 0.1s ease;\n font-size: 0.82rem;\n}\n\n.filter-option:hover {\n background: var(--mj-bg-surface-hover);\n}\n\n.filter-option-readonly {\n cursor: default;\n opacity: 0.85;\n}\n.filter-option-readonly:hover {\n background: transparent;\n}\n\n.filter-checkbox {\n width: 1rem;\n height: 1rem;\n border: 1.5px solid var(--mj-border-strong);\n border-radius: 3px;\n display: flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n font-size: 0.6rem;\n transition: all 0.15s ease;\n}\n\n.filter-checkbox-checked {\n background: var(--mj-brand-primary);\n border-color: var(--mj-brand-primary);\n color: var(--mj-text-inverse);\n}\n\n.filter-option-icon {\n color: var(--mj-text-muted);\n font-size: 0.78rem;\n width: 1rem;\n text-align: center;\n}\n\n.filter-option-label {\n flex: 1;\n color: var(--mj-text-secondary);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.filter-option-count {\n color: var(--mj-text-muted);\n font-size: 0.72rem;\n flex-shrink: 0;\n}\n\n/* Empty state */\n.filter-empty {\n padding: 1.5rem 1rem;\n text-align: center;\n color: var(--mj-text-muted);\n font-size: 0.82rem;\n}\n\n/* Relevance Slider */\n.filter-relevance-slider {\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 8px 12px;\n}\n.relevance-range {\n flex: 1;\n height: 4px;\n -webkit-appearance: none;\n appearance: none;\n background: var(--mj-border-default);\n border-radius: 2px;\n outline: none;\n cursor: pointer;\n}\n.relevance-range::-webkit-slider-thumb {\n -webkit-appearance: none;\n width: 16px;\n height: 16px;\n border-radius: 50%;\n background: var(--mj-brand-primary);\n cursor: pointer;\n border: 2px solid var(--mj-bg-surface);\n box-shadow: 0 1px 3px rgba(0,0,0,0.2);\n}\n.relevance-range::-moz-range-thumb {\n width: 16px;\n height: 16px;\n border-radius: 50%;\n background: var(--mj-brand-primary);\n cursor: pointer;\n border: 2px solid var(--mj-bg-surface);\n box-shadow: 0 1px 3px rgba(0,0,0,0.2);\n}\n.relevance-value {\n font-size: 12px;\n font-weight: 600;\n color: var(--mj-brand-primary);\n min-width: 36px;\n text-align: right;\n}\n\n/* Server threshold indicator */\n.filter-server-threshold {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 2px 12px 6px;\n font-size: 10px;\n color: var(--mj-text-muted);\n}\n.server-threshold-warning {\n color: var(--mj-status-warning-text, var(--mj-status-warning));\n font-weight: 600;\n}\n"] }]
329
329
  }], null, { Filters: [{
330
330
  type: Input
331
331
  }], ActiveFilters: [{
@@ -1 +1 @@
1
- {"version":3,"file":"search-filter.component.js","sourceRoot":"","sources":["../../src/lib/search-filter.component.ts","../../src/lib/search-filter.component.html"],"names":[],"mappings":"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","sourcesContent":["/**\n * @fileoverview Search Filter Component\n *\n * Faceted filter sidebar for narrowing search results.\n * Displays filter categories with checkable options and counts.\n */\n\nimport {\n Component,\n EventEmitter,\n Input,\n Output,\n ChangeDetectorRef,\n inject\n} from '@angular/core';\nimport { SearchFilter, SearchFilterChangeEvent } from './search-types';\n\n@Component({\n standalone: false,\n selector: 'mj-search-filter',\n templateUrl: './search-filter.component.html',\n styleUrls: ['./search-filter.component.css']\n})\nexport class SearchFilterComponent {\n private cdr = inject(ChangeDetectorRef);\n\n /** Available filters to display */\n @Input() Filters: SearchFilter[] = [];\n\n /** Currently active filter selections */\n @Input() ActiveFilters: Record<string, string[]> = {};\n\n /** Whether the filter panel is collapsible */\n @Input() Collapsible = true;\n\n /** Whether to show the min relevance slider */\n @Input() ShowRelevanceSlider = true;\n\n /** Current minimum score as a percentage (0-100) */\n @Input() MinScorePercent = 0;\n\n /** The MinScore that was last sent to the server (for visual indicator) */\n @Input() ServerMinScorePercent = 0;\n\n @Output() FilterChanged = new EventEmitter<SearchFilterChangeEvent>();\n @Output() FiltersCleared = new EventEmitter<void>();\n @Output() CloseRequested = new EventEmitter<void>();\n @Output() MinScoreChanged = new EventEmitter<number>();\n\n /** Collapsed state per category */\n public CollapsedCategories = new Set<string>();\n\n /** Toggle a filter option */\n public ToggleOption(category: string, value: string): void {\n const current = this.ActiveFilters[category] ? [...this.ActiveFilters[category]] : [];\n const index = current.indexOf(value);\n if (index >= 0) {\n current.splice(index, 1);\n } else {\n current.push(value);\n }\n // Update local state to reflect change\n this.ActiveFilters = { ...this.ActiveFilters, [category]: current };\n this.FilterChanged.emit({ Category: category, SelectedValues: current });\n this.cdr.detectChanges();\n }\n\n /** Toggle category collapse */\n public ToggleCategory(category: string): void {\n if (!this.Collapsible) return;\n if (this.CollapsedCategories.has(category)) {\n this.CollapsedCategories.delete(category);\n } else {\n this.CollapsedCategories.add(category);\n }\n this.cdr.detectChanges();\n }\n\n /** Check if a category is collapsed */\n public IsCategoryCollapsed(category: string): boolean {\n return this.CollapsedCategories.has(category);\n }\n\n /** Check if an option is selected */\n public IsOptionSelected(category: string, value: string): boolean {\n return (this.ActiveFilters[category] ?? []).includes(value);\n }\n\n /** Check if any filters are active */\n public HasActiveFilters(): boolean {\n return Object.values(this.ActiveFilters).some(v => v.length > 0);\n }\n\n /** Get total active filter count */\n public GetActiveFilterCount(): number {\n return Object.values(this.ActiveFilters).reduce((sum, v) => sum + v.length, 0);\n }\n\n /** Clear all filters */\n public ClearAll(): void {\n this.ActiveFilters = {};\n this.FiltersCleared.emit();\n this.cdr.detectChanges();\n }\n\n /** Select all options in a category, or clear all if already all selected */\n public SelectAll(category: string): void {\n const filter = this.Filters.find(f => f.Category === category);\n if (!filter) return;\n\n const current = this.ActiveFilters[category] ?? [];\n if (current.length === filter.Options.length) {\n // All selected → clear\n this.ActiveFilters[category] = [];\n } else {\n // Select all\n this.ActiveFilters[category] = filter.Options.map(o => o.Value);\n }\n this.FilterChanged.emit({ Category: category, SelectedValues: this.ActiveFilters[category] });\n this.cdr.detectChanges();\n }\n\n /** Check if all options in a category are selected */\n public IsAllSelected(category: string): boolean {\n const filter = this.Filters.find(f => f.Category === category);\n if (!filter) return false;\n const selection = this.ActiveFilters[category] ?? [];\n return selection.length === filter.Options.length;\n }\n\n /** Handle relevance slider drag (preview only — updates display without triggering search) */\n public OnMinScorePreview(value: string): void {\n this.MinScorePercent = parseInt(value, 10);\n }\n\n /** Handle relevance slider release — commits the value and triggers re-query if needed */\n public OnMinScoreCommit(): void {\n this.MinScoreChanged.emit(this.MinScorePercent);\n }\n\n /** Handle relevance slider change (legacy — used when input event fires) */\n public OnMinScoreChange(value: string): void {\n this.MinScorePercent = parseInt(value, 10);\n this.MinScoreChanged.emit(this.MinScorePercent);\n }\n}\n","<div class=\"filter-panel\">\n <div class=\"filter-panel-header\">\n <span class=\"filter-panel-title\">\n <i class=\"fa-solid fa-filter\"></i> Filters\n </span>\n <div class=\"filter-header-actions\">\n @if (HasActiveFilters()) {\n <button class=\"filter-clear-all\" (click)=\"ClearAll()\">\n Clear ({{ GetActiveFilterCount() }})\n </button>\n }\n <button class=\"filter-close-btn\" (click)=\"CloseRequested.emit()\" title=\"Hide filters\">\n <i class=\"fa-solid fa-times\"></i>\n </button>\n </div>\n </div>\n\n @for (filter of Filters; track filter.Category) {\n <div class=\"filter-category\">\n <div class=\"filter-category-header-row\">\n <div\n class=\"filter-category-header\"\n (click)=\"ToggleCategory(filter.Category)\"\n [class.filter-category-collapsed]=\"IsCategoryCollapsed(filter.Category)\"\n >\n <span class=\"filter-category-name\">{{ filter.Category }}</span>\n @if (Collapsible) {\n <i class=\"fa-solid filter-category-chevron\"\n [class.fa-chevron-down]=\"IsCategoryCollapsed(filter.Category)\"\n [class.fa-chevron-up]=\"!IsCategoryCollapsed(filter.Category)\"></i>\n }\n </div>\n @if (!IsCategoryCollapsed(filter.Category) && filter.Options.length > 1) {\n <button class=\"filter-select-all\"\n (click)=\"SelectAll(filter.Category); $event.stopPropagation()\"\n [title]=\"IsAllSelected(filter.Category) ? 'Clear all' : 'Select all'\">\n {{ IsAllSelected(filter.Category) ? 'Clear' : 'All' }}\n </button>\n }\n </div>\n\n @if (!IsCategoryCollapsed(filter.Category)) {\n <div class=\"filter-options\">\n @for (option of filter.Options; track option.Value) {\n @if (filter.Options.length === 1) {\n <!-- Single option: read-only label (no checkbox, not interactive) -->\n <div class=\"filter-option filter-option-readonly\">\n @if (option.Icon) {\n <i [class]=\"option.Icon + ' filter-option-icon'\"></i>\n }\n <span class=\"filter-option-label\">{{ option.Label }}</span>\n <span class=\"filter-option-count\">{{ option.Count }}</span>\n </div>\n } @else {\n <label class=\"filter-option\" (click)=\"ToggleOption(filter.Category, option.Value); $event.preventDefault()\">\n <span class=\"filter-checkbox\" [class.filter-checkbox-checked]=\"IsOptionSelected(filter.Category, option.Value)\">\n @if (IsOptionSelected(filter.Category, option.Value)) {\n <i class=\"fa-solid fa-check\"></i>\n }\n </span>\n @if (option.Icon) {\n <i [class]=\"option.Icon + ' filter-option-icon'\"></i>\n }\n <span class=\"filter-option-label\">{{ option.Label }}</span>\n <span class=\"filter-option-count\">{{ option.Count }}</span>\n </label>\n }\n }\n </div>\n }\n </div>\n }\n\n <!-- Relevance Threshold Slider -->\n @if (ShowRelevanceSlider) {\n <div class=\"filter-category\">\n <div class=\"filter-category-header-row\">\n <div class=\"filter-category-header\">\n <span class=\"filter-category-name\">Min Relevance</span>\n </div>\n </div>\n <div class=\"filter-relevance-slider\">\n <input\n type=\"range\"\n min=\"0\" max=\"100\" step=\"5\"\n [value]=\"MinScorePercent\"\n (input)=\"OnMinScorePreview($any($event.target).value)\"\n (mouseup)=\"OnMinScoreCommit()\"\n (touchend)=\"OnMinScoreCommit()\"\n class=\"relevance-range\" />\n <span class=\"relevance-value\">{{ MinScorePercent }}%</span>\n </div>\n @if (ServerMinScorePercent > 0) {\n <div class=\"filter-server-threshold\">\n <span class=\"server-threshold-label\">Server threshold: {{ ServerMinScorePercent }}%</span>\n @if (MinScorePercent < ServerMinScorePercent) {\n <span class=\"server-threshold-warning\">\n <i class=\"fa-solid fa-arrow-rotate-right\"></i> Will re-query\n </span>\n }\n </div>\n }\n </div>\n }\n\n @if (Filters.length === 0 && !ShowRelevanceSlider) {\n <div class=\"filter-empty\">\n <p>No filters available</p>\n </div>\n }\n</div>\n"]}
1
+ {"version":3,"file":"search-filter.component.js","sourceRoot":"","sources":["../../src/lib/search-filter.component.ts","../../src/lib/search-filter.component.html"],"names":[],"mappings":"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","sourcesContent":["/**\n * @fileoverview Search Filter Component\n *\n * Faceted filter sidebar for narrowing search results.\n * Displays filter categories with checkable options and counts.\n */\n\nimport {\n Component,\n EventEmitter,\n Input,\n Output,\n ChangeDetectorRef,\n inject\n} from '@angular/core';\nimport { SearchFilter, SearchFilterChangeEvent } from './search-types';\n\n@Component({\n standalone: false,\n selector: 'mj-search-filter',\n templateUrl: './search-filter.component.html',\n styleUrls: ['./search-filter.component.css']\n})\nexport class SearchFilterComponent {\n private cdr = inject(ChangeDetectorRef);\n\n /** Available filters to display */\n @Input() Filters: SearchFilter[] = [];\n\n /** Currently active filter selections */\n @Input() ActiveFilters: Record<string, string[]> = {};\n\n /** Whether the filter panel is collapsible */\n @Input() Collapsible = true;\n\n /** Whether to show the min relevance slider */\n @Input() ShowRelevanceSlider = true;\n\n /** Current minimum score as a percentage (0-100) */\n @Input() MinScorePercent = 0;\n\n /** The MinScore that was last sent to the server (for visual indicator) */\n @Input() ServerMinScorePercent = 0;\n\n @Output() FilterChanged = new EventEmitter<SearchFilterChangeEvent>();\n @Output() FiltersCleared = new EventEmitter<void>();\n @Output() CloseRequested = new EventEmitter<void>();\n @Output() MinScoreChanged = new EventEmitter<number>();\n\n /** Collapsed state per category */\n public CollapsedCategories = new Set<string>();\n\n /** Toggle a filter option */\n public ToggleOption(category: string, value: string): void {\n const current = this.ActiveFilters[category] ? [...this.ActiveFilters[category]] : [];\n const index = current.indexOf(value);\n if (index >= 0) {\n current.splice(index, 1);\n } else {\n current.push(value);\n }\n // Update local state to reflect change\n this.ActiveFilters = { ...this.ActiveFilters, [category]: current };\n this.FilterChanged.emit({ Category: category, SelectedValues: current });\n this.cdr.detectChanges();\n }\n\n /** Toggle category collapse */\n public ToggleCategory(category: string): void {\n if (!this.Collapsible) return;\n if (this.CollapsedCategories.has(category)) {\n this.CollapsedCategories.delete(category);\n } else {\n this.CollapsedCategories.add(category);\n }\n this.cdr.detectChanges();\n }\n\n /** Check if a category is collapsed */\n public IsCategoryCollapsed(category: string): boolean {\n return this.CollapsedCategories.has(category);\n }\n\n /** Check if an option is selected */\n public IsOptionSelected(category: string, value: string): boolean {\n return (this.ActiveFilters[category] ?? []).includes(value);\n }\n\n /** Check if any filters are active */\n public HasActiveFilters(): boolean {\n return Object.values(this.ActiveFilters).some(v => v.length > 0);\n }\n\n /** Get total active filter count */\n public GetActiveFilterCount(): number {\n return Object.values(this.ActiveFilters).reduce((sum, v) => sum + v.length, 0);\n }\n\n /** Clear all filters */\n public ClearAll(): void {\n this.ActiveFilters = {};\n this.FiltersCleared.emit();\n this.cdr.detectChanges();\n }\n\n /** Select all options in a category, or clear all if already all selected */\n public SelectAll(category: string): void {\n const filter = this.Filters.find(f => f.Category === category);\n if (!filter) return;\n\n const current = this.ActiveFilters[category] ?? [];\n if (current.length === filter.Options.length) {\n // All selected → clear\n this.ActiveFilters[category] = [];\n } else {\n // Select all\n this.ActiveFilters[category] = filter.Options.map(o => o.Value);\n }\n this.FilterChanged.emit({ Category: category, SelectedValues: this.ActiveFilters[category] });\n this.cdr.detectChanges();\n }\n\n /** Check if all options in a category are selected */\n public IsAllSelected(category: string): boolean {\n const filter = this.Filters.find(f => f.Category === category);\n if (!filter) return false;\n const selection = this.ActiveFilters[category] ?? [];\n return selection.length === filter.Options.length;\n }\n\n /** Handle relevance slider drag (preview only — updates display without triggering search) */\n public OnMinScorePreview(value: string): void {\n this.MinScorePercent = parseInt(value, 10);\n }\n\n /** Handle relevance slider release — commits the value and triggers re-query if needed */\n public OnMinScoreCommit(): void {\n this.MinScoreChanged.emit(this.MinScorePercent);\n }\n\n /** Handle relevance slider change (legacy — used when input event fires) */\n public OnMinScoreChange(value: string): void {\n this.MinScorePercent = parseInt(value, 10);\n this.MinScoreChanged.emit(this.MinScorePercent);\n }\n}\n","<div class=\"filter-panel\">\n <div class=\"filter-panel-header\">\n <span class=\"filter-panel-title\">\n <i class=\"fa-solid fa-filter\"></i> Filters\n </span>\n <div class=\"filter-header-actions\">\n @if (HasActiveFilters()) {\n <button class=\"filter-clear-all\" (click)=\"ClearAll()\">\n Clear ({{ GetActiveFilterCount() }})\n </button>\n }\n <button class=\"filter-close-btn\" (click)=\"CloseRequested.emit()\" title=\"Hide filters\" aria-label=\"Hide filters\">\n <i class=\"fa-solid fa-times\"></i>\n </button>\n </div>\n </div>\n\n @for (filter of Filters; track filter.Category) {\n <div class=\"filter-category\">\n <div class=\"filter-category-header-row\">\n <div\n class=\"filter-category-header\"\n (click)=\"ToggleCategory(filter.Category)\"\n [class.filter-category-collapsed]=\"IsCategoryCollapsed(filter.Category)\"\n >\n <span class=\"filter-category-name\">{{ filter.Category }}</span>\n @if (Collapsible) {\n <i class=\"fa-solid filter-category-chevron\"\n [class.fa-chevron-down]=\"IsCategoryCollapsed(filter.Category)\"\n [class.fa-chevron-up]=\"!IsCategoryCollapsed(filter.Category)\"></i>\n }\n </div>\n @if (!IsCategoryCollapsed(filter.Category) && filter.Options.length > 1) {\n <button class=\"filter-select-all\"\n (click)=\"SelectAll(filter.Category); $event.stopPropagation()\"\n [title]=\"IsAllSelected(filter.Category) ? 'Clear all' : 'Select all'\">\n {{ IsAllSelected(filter.Category) ? 'Clear' : 'All' }}\n </button>\n }\n </div>\n\n @if (!IsCategoryCollapsed(filter.Category)) {\n <div class=\"filter-options\">\n @for (option of filter.Options; track option.Value) {\n @if (filter.Options.length === 1) {\n <!-- Single option: read-only label (no checkbox, not interactive) -->\n <div class=\"filter-option filter-option-readonly\">\n @if (option.Icon) {\n <i [class]=\"option.Icon + ' filter-option-icon'\"></i>\n }\n <span class=\"filter-option-label\">{{ option.Label }}</span>\n <span class=\"filter-option-count\">{{ option.Count }}</span>\n </div>\n } @else {\n <label class=\"filter-option\" (click)=\"ToggleOption(filter.Category, option.Value); $event.preventDefault()\">\n <span class=\"filter-checkbox\" [class.filter-checkbox-checked]=\"IsOptionSelected(filter.Category, option.Value)\">\n @if (IsOptionSelected(filter.Category, option.Value)) {\n <i class=\"fa-solid fa-check\"></i>\n }\n </span>\n @if (option.Icon) {\n <i [class]=\"option.Icon + ' filter-option-icon'\"></i>\n }\n <span class=\"filter-option-label\">{{ option.Label }}</span>\n <span class=\"filter-option-count\">{{ option.Count }}</span>\n </label>\n }\n }\n </div>\n }\n </div>\n }\n\n <!-- Relevance Threshold Slider -->\n @if (ShowRelevanceSlider) {\n <div class=\"filter-category\">\n <div class=\"filter-category-header-row\">\n <div class=\"filter-category-header\">\n <span class=\"filter-category-name\">Min Relevance</span>\n </div>\n </div>\n <div class=\"filter-relevance-slider\">\n <input\n type=\"range\"\n min=\"0\" max=\"100\" step=\"5\"\n [value]=\"MinScorePercent\"\n (input)=\"OnMinScorePreview($any($event.target).value)\"\n (mouseup)=\"OnMinScoreCommit()\"\n (touchend)=\"OnMinScoreCommit()\"\n class=\"relevance-range\" />\n <span class=\"relevance-value\">{{ MinScorePercent }}%</span>\n </div>\n @if (ServerMinScorePercent > 0) {\n <div class=\"filter-server-threshold\">\n <span class=\"server-threshold-label\">Server threshold: {{ ServerMinScorePercent }}%</span>\n @if (MinScorePercent < ServerMinScorePercent) {\n <span class=\"server-threshold-warning\">\n <i class=\"fa-solid fa-arrow-rotate-right\"></i> Will re-query\n </span>\n }\n </div>\n }\n </div>\n }\n\n @if (Filters.length === 0 && !ShowRelevanceSlider) {\n <div class=\"filter-empty\">\n <p>No filters available</p>\n </div>\n }\n</div>\n"]}
@@ -139,7 +139,7 @@ export class SearchInputComponent {
139
139
  } if (rf & 2) {
140
140
  let _t;
141
141
  i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.inputRef = _t.first);
142
- } }, inputs: { Placeholder: "Placeholder", Query: "Query", ShowShortcutHint: "ShowShortcutHint", ShortcutHint: "ShortcutHint", DebounceMs: "DebounceMs" }, outputs: { QueryChange: "QueryChange", QuerySubmit: "QuerySubmit", InputFocused: "InputFocused", InputBlurred: "InputBlurred", InputEscaped: "InputEscaped", InputCleared: "InputCleared" }, standalone: false, decls: 6, vars: 6, consts: [["inputEl", ""], [1, "search-input-container"], [1, "fa-solid", "fa-magnifying-glass", "search-input-icon"], ["type", "text", "autocomplete", "off", "spellcheck", "false", 1, "mj-input", "search-input-field", 3, "input", "keydown", "focus", "blur", "click", "placeholder", "value"], ["title", "Clear search", "type", "button", 1, "search-input-clear"], [1, "search-input-shortcut"], ["title", "Clear search", "type", "button", 1, "search-input-clear", 3, "click"], [1, "fa-solid", "fa-times"]], template: function SearchInputComponent_Template(rf, ctx) { if (rf & 1) {
142
+ } }, inputs: { Placeholder: "Placeholder", Query: "Query", ShowShortcutHint: "ShowShortcutHint", ShortcutHint: "ShortcutHint", DebounceMs: "DebounceMs" }, outputs: { QueryChange: "QueryChange", QuerySubmit: "QuerySubmit", InputFocused: "InputFocused", InputBlurred: "InputBlurred", InputEscaped: "InputEscaped", InputCleared: "InputCleared" }, standalone: false, decls: 6, vars: 6, consts: [["inputEl", ""], [1, "search-input-container"], [1, "fa-solid", "fa-magnifying-glass", "search-input-icon"], ["type", "text", "autocomplete", "off", "spellcheck", "false", 1, "mj-input", "search-input-field", 3, "input", "keydown", "focus", "blur", "click", "placeholder", "value"], ["title", "Clear search", "aria-label", "Clear search", "type", "button", 1, "search-input-clear"], [1, "search-input-shortcut"], ["title", "Clear search", "aria-label", "Clear search", "type", "button", 1, "search-input-clear", 3, "click"], [1, "fa-solid", "fa-times"]], template: function SearchInputComponent_Template(rf, ctx) { if (rf & 1) {
143
143
  const _r1 = i0.ɵɵgetCurrentView();
144
144
  i0.ɵɵelementStart(0, "div", 1);
145
145
  i0.ɵɵelement(1, "i", 2);
@@ -161,7 +161,7 @@ export class SearchInputComponent {
161
161
  }
162
162
  (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(SearchInputComponent, [{
163
163
  type: Component,
164
- args: [{ standalone: false, selector: 'mj-search-input', template: "<div class=\"search-input-container\" [class.search-input-focused]=\"IsFocused\">\n <i class=\"fa-solid fa-magnifying-glass search-input-icon\"></i>\n <input\n #inputEl\n type=\"text\"\n class=\"mj-input search-input-field\"\n [placeholder]=\"Placeholder\"\n [value]=\"Query\"\n (input)=\"OnInput($any($event.target).value)\"\n (keydown)=\"OnKeydown($event)\"\n (focus)=\"OnFocus()\"\n (blur)=\"OnBlur()\"\n (click)=\"OnClick()\"\n autocomplete=\"off\"\n spellcheck=\"false\"\n />\n @if (Query.length > 0) {\n <button\n class=\"search-input-clear\"\n (click)=\"Clear()\"\n title=\"Clear search\"\n type=\"button\"\n >\n <i class=\"fa-solid fa-times\"></i>\n </button>\n }\n @if (ShowShortcutHint && !IsFocused && Query.length === 0) {\n <kbd class=\"search-input-shortcut\">{{ ShortcutHint }}</kbd>\n }\n</div>\n", styles: ["/* Search Input Component */\n\n.search-input-container {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n padding: 0.1rem 0.75rem;\n border: 1px solid var(--mj-border-default);\n border-radius: 8px;\n background: var(--mj-bg-surface);\n transition: border-color 0.15s ease, box-shadow 0.15s ease;\n}\n\n.search-input-focused {\n border-color: var(--mj-brand-primary);\n box-shadow: 0 0 0 3px color-mix(in srgb, var(--mj-brand-primary) 15%, transparent);\n}\n\n.search-input-icon {\n color: var(--mj-text-muted);\n font-size: 0.85rem;\n flex-shrink: 0;\n transition: color 0.15s ease;\n}\n\n.search-input-focused .search-input-icon {\n color: var(--mj-brand-primary);\n}\n\n.search-input-field {\n flex: 1;\n border: none !important;\n background: transparent !important;\n padding: 0.2rem 0 !important;\n font-size: 0.9rem;\n color: var(--mj-text-primary);\n outline: none !important;\n box-shadow: none !important;\n min-width: 0;\n}\n\n.search-input-field::placeholder {\n color: var(--mj-text-muted);\n}\n\n.search-input-clear {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 22px;\n height: 22px;\n border: none;\n border-radius: 4px;\n background: transparent;\n color: var(--mj-text-muted);\n font-size: 0.7rem;\n cursor: pointer;\n flex-shrink: 0;\n transition: background 0.1s ease, color 0.1s ease;\n}\n\n.search-input-clear:hover {\n background: var(--mj-bg-surface-hover);\n color: var(--mj-text-primary);\n}\n\n.search-input-shortcut {\n display: inline-block;\n padding: 0.12rem 0.45rem;\n border-radius: 4px;\n border: 1px solid var(--mj-border-default);\n background: var(--mj-bg-surface-sunken);\n color: var(--mj-text-muted);\n font-size: 0.7rem;\n font-family: inherit;\n flex-shrink: 0;\n line-height: 1.4;\n white-space: nowrap;\n}\n"] }]
164
+ args: [{ standalone: false, selector: 'mj-search-input', template: "<div class=\"search-input-container\" [class.search-input-focused]=\"IsFocused\">\n <i class=\"fa-solid fa-magnifying-glass search-input-icon\"></i>\n <input\n #inputEl\n type=\"text\"\n class=\"mj-input search-input-field\"\n [placeholder]=\"Placeholder\"\n [value]=\"Query\"\n (input)=\"OnInput($any($event.target).value)\"\n (keydown)=\"OnKeydown($event)\"\n (focus)=\"OnFocus()\"\n (blur)=\"OnBlur()\"\n (click)=\"OnClick()\"\n autocomplete=\"off\"\n spellcheck=\"false\"\n />\n @if (Query.length > 0) {\n <button\n class=\"search-input-clear\"\n (click)=\"Clear()\"\n title=\"Clear search\"\n aria-label=\"Clear search\"\n type=\"button\"\n >\n <i class=\"fa-solid fa-times\"></i>\n </button>\n }\n @if (ShowShortcutHint && !IsFocused && Query.length === 0) {\n <kbd class=\"search-input-shortcut\">{{ ShortcutHint }}</kbd>\n }\n</div>\n", styles: ["/* Search Input Component */\n\n.search-input-container {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n padding: 0.1rem 0.75rem;\n border: 1px solid var(--mj-border-default);\n border-radius: 8px;\n background: var(--mj-bg-surface);\n transition: border-color 0.15s ease, box-shadow 0.15s ease;\n}\n\n.search-input-focused {\n border-color: var(--mj-brand-primary);\n box-shadow: 0 0 0 3px color-mix(in srgb, var(--mj-brand-primary) 15%, transparent);\n}\n\n.search-input-icon {\n color: var(--mj-text-muted);\n font-size: 0.85rem;\n flex-shrink: 0;\n transition: color 0.15s ease;\n}\n\n.search-input-focused .search-input-icon {\n color: var(--mj-brand-primary);\n}\n\n.search-input-field {\n flex: 1;\n border: none !important;\n background: transparent !important;\n padding: 0.2rem 0 !important;\n font-size: 0.9rem;\n color: var(--mj-text-primary);\n outline: none !important;\n box-shadow: none !important;\n min-width: 0;\n}\n\n.search-input-field::placeholder {\n color: var(--mj-text-muted);\n}\n\n.search-input-clear {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 22px;\n height: 22px;\n border: none;\n border-radius: 4px;\n background: transparent;\n color: var(--mj-text-muted);\n font-size: 0.7rem;\n cursor: pointer;\n flex-shrink: 0;\n transition: background 0.1s ease, color 0.1s ease;\n}\n\n.search-input-clear:hover {\n background: var(--mj-bg-surface-hover);\n color: var(--mj-text-primary);\n}\n\n.search-input-shortcut {\n display: inline-block;\n padding: 0.12rem 0.45rem;\n border-radius: 4px;\n border: 1px solid var(--mj-border-default);\n background: var(--mj-bg-surface-sunken);\n color: var(--mj-text-muted);\n font-size: 0.7rem;\n font-family: inherit;\n flex-shrink: 0;\n line-height: 1.4;\n white-space: nowrap;\n}\n"] }]
165
165
  }], null, { inputRef: [{
166
166
  type: ViewChild,
167
167
  args: ['inputEl']
@@ -1 +1 @@
1
- {"version":3,"file":"search-input.component.js","sourceRoot":"","sources":["../../src/lib/search-input.component.ts","../../src/lib/search-input.component.html"],"names":[],"mappings":"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","sourcesContent":["/**\n * @fileoverview Search Input Component\n *\n * A reusable search text input for navbar/hero placement.\n * Features debounced query emission, keyboard shortcut hint,\n * clear button, and focus management.\n */\n\nimport {\n Component,\n ElementRef,\n EventEmitter,\n Input,\n OnDestroy,\n OnInit,\n Output,\n ViewChild,\n ChangeDetectorRef,\n inject\n} from '@angular/core';\nimport { Subject } from 'rxjs';\nimport { debounceTime, distinctUntilChanged, takeUntil } from 'rxjs/operators';\n\n@Component({\n standalone: false,\n selector: 'mj-search-input',\n templateUrl: './search-input.component.html',\n styleUrls: ['./search-input.component.css']\n})\nexport class SearchInputComponent implements OnInit, OnDestroy {\n private cdr = inject(ChangeDetectorRef);\n private destroy$ = new Subject<void>();\n private queryInput$ = new Subject<string>();\n\n @ViewChild('inputEl') inputRef!: ElementRef<HTMLInputElement>;\n\n // --- Configuration Inputs ---\n\n /** Placeholder text shown in the input */\n @Input() Placeholder = 'Search...';\n\n /** Current query value (two-way bindable via QueryChange) */\n private _Query = '';\n\n @Input()\n set Query(value: string) {\n if (value !== this._Query) {\n this._Query = value;\n this.cdr.detectChanges();\n }\n }\n get Query(): string {\n return this._Query;\n }\n\n /** Whether to show the keyboard shortcut hint badge */\n @Input() ShowShortcutHint = true;\n\n /** Text to display in the shortcut hint badge */\n @Input() ShortcutHint = 'Ctrl+K';\n\n /** Debounce time in milliseconds before QueryChange emits */\n @Input() DebounceMs = 400;\n\n // --- Outputs ---\n\n /** Emitted after debounce when query text changes */\n @Output() QueryChange = new EventEmitter<string>();\n\n /** Emitted immediately when the user presses Enter */\n @Output() QuerySubmit = new EventEmitter<string>();\n\n /** Emitted when the input receives focus */\n @Output() InputFocused = new EventEmitter<void>();\n\n /** Emitted when the input loses focus */\n @Output() InputBlurred = new EventEmitter<void>();\n\n /** Emitted when the user presses Escape in the input */\n @Output() InputEscaped = new EventEmitter<void>();\n\n /** Emitted when the user clears the input via the clear button */\n @Output() InputCleared = new EventEmitter<void>();\n\n // --- Internal state ---\n\n /** Whether the input currently has focus */\n public IsFocused = false;\n\n ngOnInit(): void {\n this.setupDebounce();\n }\n\n ngOnDestroy(): void {\n this.destroy$.next();\n this.destroy$.complete();\n }\n\n // --- Public Methods ---\n\n /** Programmatically focus the input element */\n public Focus(): void {\n this.inputRef?.nativeElement?.focus();\n }\n\n /** Clear the query and emit InputCleared */\n public Clear(): void {\n this._Query = '';\n this.queryInput$.next('');\n this.QueryChange.emit('');\n this.InputCleared.emit();\n this.Focus();\n this.cdr.detectChanges();\n }\n\n // --- Template event handlers ---\n\n /** Handle native input event */\n public OnInput(value: string): void {\n this._Query = value;\n this.queryInput$.next(value);\n }\n\n /** Handle keydown events on the input */\n public OnKeydown(event: KeyboardEvent): void {\n switch (event.key) {\n case 'Enter':\n event.preventDefault();\n this.QuerySubmit.emit(this._Query);\n break;\n case 'Escape':\n event.preventDefault();\n this.InputEscaped.emit();\n break;\n }\n }\n\n /** Handle focus event */\n public OnFocus(): void {\n this.IsFocused = true;\n this.InputFocused.emit();\n this.cdr.detectChanges();\n }\n\n /** Handle click — re-opens suggest when input already has focus */\n public OnClick(): void {\n if (this.IsFocused) {\n this.InputFocused.emit();\n }\n }\n\n /** Handle blur event */\n public OnBlur(): void {\n this.IsFocused = false;\n this.InputBlurred.emit();\n this.cdr.detectChanges();\n }\n\n // --- Private ---\n\n private setupDebounce(): void {\n this.queryInput$\n .pipe(\n debounceTime(this.DebounceMs),\n distinctUntilChanged(),\n takeUntil(this.destroy$)\n )\n .subscribe(value => {\n this.QueryChange.emit(value);\n });\n }\n}\n","<div class=\"search-input-container\" [class.search-input-focused]=\"IsFocused\">\n <i class=\"fa-solid fa-magnifying-glass search-input-icon\"></i>\n <input\n #inputEl\n type=\"text\"\n class=\"mj-input search-input-field\"\n [placeholder]=\"Placeholder\"\n [value]=\"Query\"\n (input)=\"OnInput($any($event.target).value)\"\n (keydown)=\"OnKeydown($event)\"\n (focus)=\"OnFocus()\"\n (blur)=\"OnBlur()\"\n (click)=\"OnClick()\"\n autocomplete=\"off\"\n spellcheck=\"false\"\n />\n @if (Query.length > 0) {\n <button\n class=\"search-input-clear\"\n (click)=\"Clear()\"\n title=\"Clear search\"\n type=\"button\"\n >\n <i class=\"fa-solid fa-times\"></i>\n </button>\n }\n @if (ShowShortcutHint && !IsFocused && Query.length === 0) {\n <kbd class=\"search-input-shortcut\">{{ ShortcutHint }}</kbd>\n }\n</div>\n"]}
1
+ {"version":3,"file":"search-input.component.js","sourceRoot":"","sources":["../../src/lib/search-input.component.ts","../../src/lib/search-input.component.html"],"names":[],"mappings":"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","sourcesContent":["/**\n * @fileoverview Search Input Component\n *\n * A reusable search text input for navbar/hero placement.\n * Features debounced query emission, keyboard shortcut hint,\n * clear button, and focus management.\n */\n\nimport {\n Component,\n ElementRef,\n EventEmitter,\n Input,\n OnDestroy,\n OnInit,\n Output,\n ViewChild,\n ChangeDetectorRef,\n inject\n} from '@angular/core';\nimport { Subject } from 'rxjs';\nimport { debounceTime, distinctUntilChanged, takeUntil } from 'rxjs/operators';\n\n@Component({\n standalone: false,\n selector: 'mj-search-input',\n templateUrl: './search-input.component.html',\n styleUrls: ['./search-input.component.css']\n})\nexport class SearchInputComponent implements OnInit, OnDestroy {\n private cdr = inject(ChangeDetectorRef);\n private destroy$ = new Subject<void>();\n private queryInput$ = new Subject<string>();\n\n @ViewChild('inputEl') inputRef!: ElementRef<HTMLInputElement>;\n\n // --- Configuration Inputs ---\n\n /** Placeholder text shown in the input */\n @Input() Placeholder = 'Search...';\n\n /** Current query value (two-way bindable via QueryChange) */\n private _Query = '';\n\n @Input()\n set Query(value: string) {\n if (value !== this._Query) {\n this._Query = value;\n this.cdr.detectChanges();\n }\n }\n get Query(): string {\n return this._Query;\n }\n\n /** Whether to show the keyboard shortcut hint badge */\n @Input() ShowShortcutHint = true;\n\n /** Text to display in the shortcut hint badge */\n @Input() ShortcutHint = 'Ctrl+K';\n\n /** Debounce time in milliseconds before QueryChange emits */\n @Input() DebounceMs = 400;\n\n // --- Outputs ---\n\n /** Emitted after debounce when query text changes */\n @Output() QueryChange = new EventEmitter<string>();\n\n /** Emitted immediately when the user presses Enter */\n @Output() QuerySubmit = new EventEmitter<string>();\n\n /** Emitted when the input receives focus */\n @Output() InputFocused = new EventEmitter<void>();\n\n /** Emitted when the input loses focus */\n @Output() InputBlurred = new EventEmitter<void>();\n\n /** Emitted when the user presses Escape in the input */\n @Output() InputEscaped = new EventEmitter<void>();\n\n /** Emitted when the user clears the input via the clear button */\n @Output() InputCleared = new EventEmitter<void>();\n\n // --- Internal state ---\n\n /** Whether the input currently has focus */\n public IsFocused = false;\n\n ngOnInit(): void {\n this.setupDebounce();\n }\n\n ngOnDestroy(): void {\n this.destroy$.next();\n this.destroy$.complete();\n }\n\n // --- Public Methods ---\n\n /** Programmatically focus the input element */\n public Focus(): void {\n this.inputRef?.nativeElement?.focus();\n }\n\n /** Clear the query and emit InputCleared */\n public Clear(): void {\n this._Query = '';\n this.queryInput$.next('');\n this.QueryChange.emit('');\n this.InputCleared.emit();\n this.Focus();\n this.cdr.detectChanges();\n }\n\n // --- Template event handlers ---\n\n /** Handle native input event */\n public OnInput(value: string): void {\n this._Query = value;\n this.queryInput$.next(value);\n }\n\n /** Handle keydown events on the input */\n public OnKeydown(event: KeyboardEvent): void {\n switch (event.key) {\n case 'Enter':\n event.preventDefault();\n this.QuerySubmit.emit(this._Query);\n break;\n case 'Escape':\n event.preventDefault();\n this.InputEscaped.emit();\n break;\n }\n }\n\n /** Handle focus event */\n public OnFocus(): void {\n this.IsFocused = true;\n this.InputFocused.emit();\n this.cdr.detectChanges();\n }\n\n /** Handle click — re-opens suggest when input already has focus */\n public OnClick(): void {\n if (this.IsFocused) {\n this.InputFocused.emit();\n }\n }\n\n /** Handle blur event */\n public OnBlur(): void {\n this.IsFocused = false;\n this.InputBlurred.emit();\n this.cdr.detectChanges();\n }\n\n // --- Private ---\n\n private setupDebounce(): void {\n this.queryInput$\n .pipe(\n debounceTime(this.DebounceMs),\n distinctUntilChanged(),\n takeUntil(this.destroy$)\n )\n .subscribe(value => {\n this.QueryChange.emit(value);\n });\n }\n}\n","<div class=\"search-input-container\" [class.search-input-focused]=\"IsFocused\">\n <i class=\"fa-solid fa-magnifying-glass search-input-icon\"></i>\n <input\n #inputEl\n type=\"text\"\n class=\"mj-input search-input-field\"\n [placeholder]=\"Placeholder\"\n [value]=\"Query\"\n (input)=\"OnInput($any($event.target).value)\"\n (keydown)=\"OnKeydown($event)\"\n (focus)=\"OnFocus()\"\n (blur)=\"OnBlur()\"\n (click)=\"OnClick()\"\n autocomplete=\"off\"\n spellcheck=\"false\"\n />\n @if (Query.length > 0) {\n <button\n class=\"search-input-clear\"\n (click)=\"Clear()\"\n title=\"Clear search\"\n aria-label=\"Clear search\"\n type=\"button\"\n >\n <i class=\"fa-solid fa-times\"></i>\n </button>\n }\n @if (ShowShortcutHint && !IsFocused && Query.length === 0) {\n <kbd class=\"search-input-shortcut\">{{ ShortcutHint }}</kbd>\n }\n</div>\n"]}
@@ -430,6 +430,7 @@ function SearchResultsComponent_ng_template_3_Template(rf, ctx) { if (rf & 1) {
430
430
  i0.ɵɵconditional(ctx_r0.ShowScores ? 8 : -1);
431
431
  i0.ɵɵadvance(2);
432
432
  i0.ɵɵproperty("title", result_r12.ResultType === "storage-file" ? "Open file" : "Open record");
433
+ i0.ɵɵattribute("aria-label", result_r12.ResultType === "storage-file" ? "Open file" : "Open record");
433
434
  i0.ɵɵadvance();
434
435
  i0.ɵɵclassMap(result_r12.ResultType === "storage-file" ? "fa-solid fa-external-link" : "fa-solid fa-arrow-up-right-from-square");
435
436
  i0.ɵɵadvance(2);
@@ -690,16 +691,16 @@ export class SearchResultsComponent {
690
691
  }
691
692
  }
692
693
  static ɵfac = function SearchResultsComponent_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || SearchResultsComponent)(); };
693
- static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: SearchResultsComponent, selectors: [["mj-search-results"]], inputs: { ResultGroups: "ResultGroups", FlatResults: "FlatResults", DisplayMode: "DisplayMode", IsLoading: "IsLoading", TotalCount: "TotalCount", ElapsedMs: "ElapsedMs", MaxPerGroup: "MaxPerGroup", PageSize: "PageSize", ShowSummary: "ShowSummary", ShowScores: "ShowScores", ShowTags: "ShowTags", ShowSourceIcons: "ShowSourceIcons", HighlightText: "HighlightText" }, outputs: { ResultSelected: "ResultSelected", OpenRecordRequested: "OpenRecordRequested", MoreLikeThisRequested: "MoreLikeThisRequested" }, standalone: false, decls: 5, vars: 1, consts: [["resultCardTpl", ""], [1, "results-loading"], [1, "results-empty"], ["text", "Searching...", "size", "medium"], [1, "fa-solid", "fa-search", "results-empty-icon"], [1, "results-empty-text"], [1, "results-summary"], [1, "results-summary-time"], [1, "results-scroll-area"], [1, "results-pager"], [4, "ngTemplateOutlet", "ngTemplateOutletContext"], [1, "pager-btn", "pager-prev", 3, "click", "disabled"], [1, "fa-solid", "fa-chevron-left"], [1, "pager-btn", "pager-next", 3, "click", "disabled"], [1, "fa-solid", "fa-chevron-right"], [1, "pager-ellipsis"], [1, "pager-btn", "pager-num", 3, "pager-active"], [1, "pager-btn", "pager-num", 3, "click"], [1, "results-group"], [1, "results-group-header", 3, "click"], [3, "class"], [1, "results-group-label"], [1, "results-group-count"], [1, "fa-solid", "results-group-chevron"], [1, "results-show-more"], [1, "results-show-more", 3, "click"], [1, "fa-solid", "fa-chevron-down"], [1, "result-card"], [1, "result-card-main", 3, "click"], [1, "result-card-icon-wrap"], [1, "result-card-body"], [1, "result-card-title", 3, "innerHTML"], [1, "result-card-entity", 3, "innerHTML"], [1, "result-card-tags"], [1, "result-card-score"], [1, "result-card-actions"], [1, "result-open-btn", 3, "click", "title"], ["title", "Expand details", 1, "result-expand-btn", 3, "click"], [1, "fa-solid"], [1, "result-card-detail"], [1, "result-tag-chip", 3, "innerHTML"], [1, "result-tag-more"], [1, "score-circle"], [1, "score-value"], [1, "detail-section"], [1, "detail-meta"], [1, "detail-meta-item"], [1, "fa-solid", "fa-layer-group"], [1, "fa-solid", "fa-fingerprint"], [1, "detail-record-id"], [1, "fa-solid", "fa-signal"], [1, "detail-tags"], [1, "detail-links"], [1, "detail-actions"], [1, "detail-open-btn", 3, "click"], [1, "detail-similar-btn", 3, "click"], [1, "fa-solid", "fa-shuffle"], [1, "detail-label"], [1, "detail-snippet", 3, "innerHTML"], [1, "fa-solid", "fa-chart-bar"], [1, "result-card-tag", 3, "innerHTML"], ["target", "_blank", 1, "detail-link", 3, "href"], [1, "detail-link", "detail-link-email", 3, "href"], ["target", "_blank", 1, "detail-link", 3, "click", "href"], [1, "fa-solid", "fa-arrow-up-right-from-square"], [1, "detail-link", "detail-link-email", 3, "click", "href"], [1, "fa-solid", "fa-envelope"], [1, "fa-solid", "fa-external-link"]], template: function SearchResultsComponent_Template(rf, ctx) { if (rf & 1) {
694
+ static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: SearchResultsComponent, selectors: [["mj-search-results"]], inputs: { ResultGroups: "ResultGroups", FlatResults: "FlatResults", DisplayMode: "DisplayMode", IsLoading: "IsLoading", TotalCount: "TotalCount", ElapsedMs: "ElapsedMs", MaxPerGroup: "MaxPerGroup", PageSize: "PageSize", ShowSummary: "ShowSummary", ShowScores: "ShowScores", ShowTags: "ShowTags", ShowSourceIcons: "ShowSourceIcons", HighlightText: "HighlightText" }, outputs: { ResultSelected: "ResultSelected", OpenRecordRequested: "OpenRecordRequested", MoreLikeThisRequested: "MoreLikeThisRequested" }, standalone: false, decls: 5, vars: 1, consts: [["resultCardTpl", ""], [1, "results-loading"], [1, "results-empty"], ["text", "Searching...", "size", "medium"], [1, "fa-solid", "fa-search", "results-empty-icon"], [1, "results-empty-text"], [1, "results-summary"], [1, "results-summary-time"], [1, "results-scroll-area"], [1, "results-pager"], [4, "ngTemplateOutlet", "ngTemplateOutletContext"], ["aria-label", "Previous page", 1, "pager-btn", "pager-prev", 3, "click", "disabled"], [1, "fa-solid", "fa-chevron-left"], ["aria-label", "Next page", 1, "pager-btn", "pager-next", 3, "click", "disabled"], [1, "fa-solid", "fa-chevron-right"], [1, "pager-ellipsis"], [1, "pager-btn", "pager-num", 3, "pager-active"], [1, "pager-btn", "pager-num", 3, "click"], [1, "results-group"], [1, "results-group-header", 3, "click"], [3, "class"], [1, "results-group-label"], [1, "results-group-count"], [1, "fa-solid", "results-group-chevron"], [1, "results-show-more"], [1, "results-show-more", 3, "click"], [1, "fa-solid", "fa-chevron-down"], [1, "result-card"], [1, "result-card-main", 3, "click"], [1, "result-card-icon-wrap"], [1, "result-card-body"], [1, "result-card-title", 3, "innerHTML"], [1, "result-card-entity", 3, "innerHTML"], [1, "result-card-tags"], [1, "result-card-score"], [1, "result-card-actions"], [1, "result-open-btn", 3, "click", "title"], ["title", "Expand details", "aria-label", "Expand details", 1, "result-expand-btn", 3, "click"], [1, "fa-solid"], [1, "result-card-detail"], [1, "result-tag-chip", 3, "innerHTML"], [1, "result-tag-more"], [1, "score-circle"], [1, "score-value"], [1, "detail-section"], [1, "detail-meta"], [1, "detail-meta-item"], [1, "fa-solid", "fa-layer-group"], [1, "fa-solid", "fa-fingerprint"], [1, "detail-record-id"], [1, "fa-solid", "fa-signal"], [1, "detail-tags"], [1, "detail-links"], [1, "detail-actions"], [1, "detail-open-btn", 3, "click"], [1, "detail-similar-btn", 3, "click"], [1, "fa-solid", "fa-shuffle"], [1, "detail-label"], [1, "detail-snippet", 3, "innerHTML"], [1, "fa-solid", "fa-chart-bar"], [1, "result-card-tag", 3, "innerHTML"], ["target", "_blank", 1, "detail-link", 3, "href"], [1, "detail-link", "detail-link-email", 3, "href"], ["target", "_blank", 1, "detail-link", 3, "click", "href"], [1, "fa-solid", "fa-arrow-up-right-from-square"], [1, "detail-link", "detail-link-email", 3, "click", "href"], [1, "fa-solid", "fa-envelope"], [1, "fa-solid", "fa-external-link"]], template: function SearchResultsComponent_Template(rf, ctx) { if (rf & 1) {
694
695
  i0.ɵɵconditionalCreate(0, SearchResultsComponent_Conditional_0_Template, 2, 0, "div", 1)(1, SearchResultsComponent_Conditional_1_Template, 4, 0, "div", 2)(2, SearchResultsComponent_Conditional_2_Template, 3, 2);
695
- i0.ɵɵtemplate(3, SearchResultsComponent_ng_template_3_Template, 15, 16, "ng-template", null, 0, i0.ɵɵtemplateRefExtractor);
696
+ i0.ɵɵtemplate(3, SearchResultsComponent_ng_template_3_Template, 15, 17, "ng-template", null, 0, i0.ɵɵtemplateRefExtractor);
696
697
  } if (rf & 2) {
697
698
  i0.ɵɵconditional(ctx.IsLoading ? 0 : (ctx.DisplayMode === "flat" ? ctx.FlatResults.length : ctx.ResultGroups.length) === 0 && ctx.TotalCount === 0 ? 1 : 2);
698
699
  } }, dependencies: [i1.NgTemplateOutlet, i2.LoadingComponent], styles: ["\n\n\n[_nghost-%COMP%] {\n display: flex;\n flex-direction: column;\n min-height: 0;\n flex: 1;\n}\n\n.results-scroll-area[_ngcontent-%COMP%] {\n flex: 1;\n overflow-y: auto;\n min-height: 0;\n}\n\n.results-loading[_ngcontent-%COMP%] {\n display: flex;\n justify-content: center;\n padding: 3rem 1rem;\n}\n\n.results-empty[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n align-items: center;\n padding: 3rem 1rem;\n gap: 0.5rem;\n}\n\n.results-empty-icon[_ngcontent-%COMP%] {\n font-size: 2rem;\n color: var(--mj-text-muted);\n margin-bottom: 0.5rem;\n}\n\n.results-empty-text[_ngcontent-%COMP%] {\n color: var(--mj-text-muted);\n font-size: 0.9rem;\n}\n\n.results-summary[_ngcontent-%COMP%] {\n padding: 0.75rem 1rem;\n color: var(--mj-text-secondary);\n font-size: 0.85rem;\n border-bottom: 1px solid var(--mj-border-subtle);\n}\n\n.results-summary-time[_ngcontent-%COMP%] {\n color: var(--mj-text-muted);\n}\n\n\n\n.results-group[_ngcontent-%COMP%] {\n border-bottom: 1px solid var(--mj-border-subtle);\n}\n\n.results-group-header[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n padding: 0.75rem 1rem;\n cursor: pointer;\n transition: background 0.1s ease;\n}\n\n.results-group-header[_ngcontent-%COMP%]:hover {\n background: var(--mj-bg-surface-hover);\n}\n\n.results-group-icon[_ngcontent-%COMP%] {\n color: var(--mj-brand-primary);\n font-size: 0.85rem;\n}\n\n.results-group-label[_ngcontent-%COMP%] {\n font-weight: 600;\n font-size: 0.85rem;\n color: var(--mj-text-primary);\n flex: 1;\n}\n\n.results-group-count[_ngcontent-%COMP%] {\n font-size: 0.78rem;\n color: var(--mj-text-muted);\n}\n\n.results-group-chevron[_ngcontent-%COMP%] {\n color: var(--mj-text-muted);\n font-size: 0.7rem;\n transition: transform 0.2s ease;\n}\n\n\n\n.result-card[_ngcontent-%COMP%] {\n border: 1px solid var(--mj-border-subtle);\n border-radius: 8px;\n margin: 0 0 0.35rem 1.5rem;\n overflow: hidden;\n transition: border-color 0.15s, box-shadow 0.15s;\n}\n\n.result-card[_ngcontent-%COMP%]:hover {\n border-color: var(--mj-border-default);\n}\n\n.result-card-expanded[_ngcontent-%COMP%] {\n border-color: var(--mj-brand-primary);\n box-shadow: 0 2px 8px color-mix(in srgb, var(--mj-brand-primary) 10%, transparent);\n}\n\n.result-card-main[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n padding: 0.65rem 0.85rem;\n cursor: pointer;\n transition: background 0.1s ease;\n}\n\n.result-card-main[_ngcontent-%COMP%]:hover {\n background: var(--mj-bg-surface-hover);\n}\n\n.result-card-icon-wrap[_ngcontent-%COMP%] {\n width: 2rem;\n height: 2rem;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 6px;\n background: color-mix(in srgb, var(--mj-brand-primary) 10%, var(--mj-bg-surface));\n color: var(--mj-brand-primary);\n font-size: 0.8rem;\n flex-shrink: 0;\n}\n\n.result-card-body[_ngcontent-%COMP%] {\n flex: 1;\n min-width: 0;\n}\n\n.result-card-title[_ngcontent-%COMP%] {\n color: var(--mj-text-primary);\n font-weight: 600;\n font-size: 0.85rem;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.result-card-entity[_ngcontent-%COMP%] {\n color: var(--mj-text-muted);\n font-size: 0.7rem;\n}\n\n.result-card-tags[_ngcontent-%COMP%] {\n display: flex;\n flex-wrap: wrap;\n gap: 4px;\n margin-top: 4px;\n}\n\n.result-tag-chip[_ngcontent-%COMP%] {\n display: inline-block;\n padding: 1px 7px;\n border-radius: 10px;\n font-size: 0.65rem;\n font-weight: 500;\n background: color-mix(in srgb, var(--mj-brand-primary) 10%, var(--mj-bg-surface));\n color: var(--mj-brand-primary);\n border: 1px solid color-mix(in srgb, var(--mj-brand-primary) 20%, var(--mj-border-default));\n}\n\n.result-tag-more[_ngcontent-%COMP%] {\n display: inline-block;\n padding: 1px 6px;\n font-size: 0.6rem;\n color: var(--mj-text-muted);\n}\n\n.result-card-actions[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 4px;\n flex-shrink: 0;\n}\n\n.result-open-btn[_ngcontent-%COMP%], \n.result-expand-btn[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n border: 1px solid var(--mj-border-default);\n border-radius: 6px;\n background: transparent;\n color: var(--mj-text-muted);\n font-size: 11px;\n cursor: pointer;\n transition: all 0.15s;\n}\n\n.result-open-btn[_ngcontent-%COMP%]:hover {\n background: color-mix(in srgb, var(--mj-brand-primary) 10%, var(--mj-bg-surface));\n border-color: var(--mj-brand-primary);\n color: var(--mj-brand-primary);\n}\n\n.result-expand-btn[_ngcontent-%COMP%]:hover {\n background: var(--mj-bg-surface-hover);\n color: var(--mj-text-primary);\n}\n\n\n\n.result-card-detail[_ngcontent-%COMP%] {\n padding: 0.75rem 1rem 0.85rem 3.5rem;\n border-top: 1px solid var(--mj-border-subtle);\n background: var(--mj-bg-surface-card);\n}\n\n.detail-section[_ngcontent-%COMP%] {\n margin-bottom: 0.65rem;\n}\n\n.detail-label[_ngcontent-%COMP%] {\n font-size: 0.7rem;\n font-weight: 600;\n color: var(--mj-text-muted);\n text-transform: uppercase;\n letter-spacing: 0.04em;\n margin-bottom: 0.25rem;\n display: block;\n}\n\n.detail-snippet[_ngcontent-%COMP%] {\n margin: 0;\n font-size: 0.82rem;\n color: var(--mj-text-secondary);\n line-height: 1.5;\n}\n\n.detail-meta[_ngcontent-%COMP%] {\n display: flex;\n flex-wrap: wrap;\n gap: 0.75rem;\n margin-bottom: 0.65rem;\n}\n\n.detail-meta-item[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 5px;\n font-size: 0.75rem;\n color: var(--mj-text-muted);\n}\n\n.detail-meta-item[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n font-size: 0.65rem;\n}\n\n.detail-record-id[_ngcontent-%COMP%] {\n font-family: monospace;\n font-size: 0.7rem;\n max-width: 180px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.detail-tags[_ngcontent-%COMP%] {\n display: flex;\n flex-wrap: wrap;\n gap: 4px;\n margin-bottom: 0.65rem;\n}\n\n.detail-links[_ngcontent-%COMP%] {\n display: flex;\n flex-wrap: wrap;\n gap: 6px;\n margin-bottom: 8px;\n}\n\n.detail-link[_ngcontent-%COMP%] {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n padding: 3px 8px;\n border-radius: 6px;\n font-size: 0.72rem;\n color: var(--mj-text-link);\n background: color-mix(in srgb, var(--mj-brand-primary) 5%, var(--mj-bg-surface));\n border: 1px solid color-mix(in srgb, var(--mj-brand-primary) 15%, var(--mj-border-default));\n text-decoration: none;\n transition: background 0.15s ease;\n}\n\n.detail-link[_ngcontent-%COMP%]:hover {\n background: color-mix(in srgb, var(--mj-brand-primary) 12%, var(--mj-bg-surface));\n text-decoration: underline;\n}\n\n.detail-link[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n font-size: 0.6rem;\n}\n\n.detail-actions[_ngcontent-%COMP%] {\n display: flex;\n gap: 8px;\n}\n\n.detail-open-btn[_ngcontent-%COMP%] {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n padding: 6px 14px;\n border: 1px solid var(--mj-brand-primary);\n border-radius: 6px;\n background: transparent;\n color: var(--mj-brand-primary);\n font-size: 0.78rem;\n font-weight: 500;\n cursor: pointer;\n transition: all 0.15s;\n}\n\n.detail-open-btn[_ngcontent-%COMP%]:hover {\n background: color-mix(in srgb, var(--mj-brand-primary) 10%, var(--mj-bg-surface));\n}\n\n.detail-similar-btn[_ngcontent-%COMP%] {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n padding: 6px 14px;\n border: 1px solid var(--mj-border-default);\n border-radius: 6px;\n background: transparent;\n color: var(--mj-text-secondary);\n font-size: 0.78rem;\n font-weight: 500;\n cursor: pointer;\n transition: all 0.15s;\n}\n\n.detail-similar-btn[_ngcontent-%COMP%]:hover {\n border-color: var(--mj-brand-primary);\n color: var(--mj-brand-primary);\n background: color-mix(in srgb, var(--mj-brand-primary) 5%, var(--mj-bg-surface));\n}\n\n.result-card-snippet[_ngcontent-%COMP%] {\n color: var(--mj-text-secondary);\n font-size: 0.82rem;\n line-height: 1.4;\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n overflow: hidden;\n}\n\n.result-card-tags[_ngcontent-%COMP%] {\n display: flex;\n flex-wrap: wrap;\n gap: 0.25rem;\n margin-top: 0.35rem;\n}\n\n.result-card-tag[_ngcontent-%COMP%] {\n display: inline-block;\n padding: 0.1rem 0.4rem;\n border-radius: 4px;\n background: var(--mj-bg-surface-sunken);\n color: var(--mj-text-muted);\n font-size: 0.7rem;\n}\n\n.result-card-tag-more[_ngcontent-%COMP%] {\n color: var(--mj-brand-primary);\n}\n\n.result-card-score[_ngcontent-%COMP%] {\n flex-shrink: 0;\n margin-top: 0.2rem;\n}\n\n.score-circle[_ngcontent-%COMP%] {\n width: 2.5rem;\n height: 2.5rem;\n border-radius: 50%;\n display: flex;\n align-items: center;\n justify-content: center;\n \n\n background: color-mix(in srgb, var(--mj-text-muted) 10%, var(--mj-bg-surface));\n border: 2px solid color-mix(in srgb, var(--mj-text-muted) 25%, var(--mj-bg-surface));\n}\n.score-circle.score-high[_ngcontent-%COMP%] {\n background: color-mix(in srgb, var(--mj-status-success) 15%, var(--mj-bg-surface));\n border-color: color-mix(in srgb, var(--mj-status-success) 40%, var(--mj-bg-surface));\n}\n.score-circle.score-medium[_ngcontent-%COMP%] {\n background: color-mix(in srgb, var(--mj-status-warning) 15%, var(--mj-bg-surface));\n border-color: color-mix(in srgb, var(--mj-status-warning) 40%, var(--mj-bg-surface));\n}\n.score-circle.score-low[_ngcontent-%COMP%] {\n background: color-mix(in srgb, var(--mj-text-muted) 10%, var(--mj-bg-surface));\n border-color: color-mix(in srgb, var(--mj-text-muted) 25%, var(--mj-bg-surface));\n}\n\n.score-value[_ngcontent-%COMP%] {\n font-size: 0.7rem;\n font-weight: 700;\n color: var(--mj-text-secondary);\n}\n.score-high[_ngcontent-%COMP%] .score-value[_ngcontent-%COMP%] {\n color: var(--mj-status-success-text, var(--mj-status-success));\n}\n.score-medium[_ngcontent-%COMP%] .score-value[_ngcontent-%COMP%] {\n color: var(--mj-status-warning-text, var(--mj-status-warning));\n}\n.score-low[_ngcontent-%COMP%] .score-value[_ngcontent-%COMP%] {\n color: var(--mj-text-muted);\n}\n\n\n\n.results-show-more[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n width: 100%;\n padding: 0.5rem 1rem;\n border: none;\n background: transparent;\n color: var(--mj-brand-primary);\n font-size: 0.82rem;\n cursor: pointer;\n justify-content: center;\n transition: background 0.1s ease;\n}\n\n.results-show-more[_ngcontent-%COMP%]:hover {\n background: var(--mj-bg-surface-hover);\n}\n\n\n\n.results-pager[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 4px;\n padding: 0.75rem 0;\n flex-shrink: 0;\n border-top: 1px solid var(--mj-border-subtle);\n background: var(--mj-bg-surface);\n}\n\n.pager-btn[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n justify-content: center;\n min-width: 32px;\n height: 32px;\n border: 1px solid var(--mj-border-default);\n border-radius: 8px;\n background: var(--mj-bg-surface);\n color: var(--mj-text-secondary);\n font-size: 0.82rem;\n font-weight: 500;\n cursor: pointer;\n transition: all 0.15s ease;\n padding: 0 6px;\n}\n\n.pager-btn[_ngcontent-%COMP%]:hover:not(:disabled) {\n background: var(--mj-bg-surface-hover);\n border-color: var(--mj-brand-primary);\n color: var(--mj-brand-primary);\n}\n\n.pager-btn[_ngcontent-%COMP%]:disabled {\n opacity: 0.35;\n cursor: default;\n}\n\n.pager-active[_ngcontent-%COMP%] {\n background: var(--mj-brand-primary);\n border-color: var(--mj-brand-primary);\n color: var(--mj-text-inverse);\n}\n\n.pager-active[_ngcontent-%COMP%]:hover:not(:disabled) {\n background: var(--mj-brand-primary-hover);\n border-color: var(--mj-brand-primary-hover);\n color: var(--mj-text-inverse);\n}\n\n.pager-ellipsis[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 28px;\n color: var(--mj-text-muted);\n font-size: 0.8rem;\n}\n\n.pager-prev[_ngcontent-%COMP%] i[_ngcontent-%COMP%], \n.pager-next[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n font-size: 0.7rem;\n}\n\n\n\n[_nghost-%COMP%] .search-highlight, \n.search-highlight[_ngcontent-%COMP%] {\n background: color-mix(in srgb, var(--mj-status-warning) 35%, transparent);\n color: inherit;\n padding: 1px 2px;\n border-radius: 2px;\n}"] });
699
700
  }
700
701
  (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(SearchResultsComponent, [{
701
702
  type: Component,
702
- args: [{ standalone: false, selector: 'mj-search-results', template: "@if (IsLoading) {\n <div class=\"results-loading\">\n <mj-loading text=\"Searching...\" size=\"medium\"></mj-loading>\n </div>\n} @else if ((DisplayMode === 'flat' ? FlatResults.length : ResultGroups.length) === 0 && TotalCount === 0) {\n <div class=\"results-empty\">\n <i class=\"fa-solid fa-search results-empty-icon\"></i>\n <p class=\"results-empty-text\">No results to display</p>\n </div>\n} @else {\n <!-- Results summary -->\n @if (ShowSummary) {\n <div class=\"results-summary\">\n <span>{{ TotalCount }} results</span>\n @if (ElapsedMs > 0) {\n <span class=\"results-summary-time\"> in {{ FormatTime(ElapsedMs) }}</span>\n }\n </div>\n }\n\n @if (DisplayMode === 'flat') {\n <!-- \u2550\u2550\u2550\u2550\u2550\u2550 FLAT BLENDED LIST (sorted by score) \u2550\u2550\u2550\u2550\u2550\u2550 -->\n <div class=\"results-scroll-area\">\n @for (result of PagedResults; track result.ID) {\n <ng-container *ngTemplateOutlet=\"resultCardTpl; context: { $implicit: result }\"></ng-container>\n }\n </div>\n\n <!-- Google-style pagination (sticky at bottom) -->\n @if (TotalPages > 1) {\n <div class=\"results-pager\">\n <button class=\"pager-btn pager-prev\" [disabled]=\"CurrentPage === 1\"\n (click)=\"GoToPage(CurrentPage - 1)\">\n <i class=\"fa-solid fa-chevron-left\"></i>\n </button>\n @for (p of PageNumbers; track $index) {\n @if (p === -1) {\n <span class=\"pager-ellipsis\">...</span>\n } @else {\n <button class=\"pager-btn pager-num\" [class.pager-active]=\"p === CurrentPage\"\n (click)=\"GoToPage(p)\">{{ p }}</button>\n }\n }\n <button class=\"pager-btn pager-next\" [disabled]=\"CurrentPage === TotalPages\"\n (click)=\"GoToPage(CurrentPage + 1)\">\n <i class=\"fa-solid fa-chevron-right\"></i>\n </button>\n </div>\n }\n } @else {\n <!-- \u2550\u2550\u2550\u2550\u2550\u2550 GROUPED BY SOURCE TYPE \u2550\u2550\u2550\u2550\u2550\u2550 -->\n @for (group of ResultGroups; track group.SourceType) {\n <div class=\"results-group\">\n <div class=\"results-group-header\" (click)=\"ToggleGroup(group.SourceType)\">\n @if (ShowSourceIcons) {\n <i [class]=\"group.Icon + ' results-group-icon'\"></i>\n }\n <span class=\"results-group-label\">{{ group.Label }}</span>\n <span class=\"results-group-count\">({{ group.TotalCount }})</span>\n <i class=\"fa-solid results-group-chevron\"\n [class.fa-chevron-down]=\"!IsGroupExpanded(group.SourceType)\"\n [class.fa-chevron-up]=\"IsGroupExpanded(group.SourceType)\"></i>\n </div>\n\n @for (result of GetVisibleResults(group); track result.ID) {\n <ng-container *ngTemplateOutlet=\"resultCardTpl; context: { $implicit: result }\"></ng-container>\n }\n\n @if (HasMoreResults(group)) {\n <button class=\"results-show-more\" (click)=\"ToggleGroup(group.SourceType)\">\n <i class=\"fa-solid fa-chevron-down\"></i>\n Show {{ GetHiddenCount(group) }} more results\n </button>\n }\n </div>\n }\n }\n}\n\n<!-- \u2550\u2550\u2550 Shared result card template \u2550\u2550\u2550 -->\n<ng-template #resultCardTpl let-result>\n <div class=\"result-card\" [class.result-card-expanded]=\"IsExpanded(result.ID)\">\n <!-- Main row -->\n <div class=\"result-card-main\" (click)=\"ToggleExpand(result)\">\n <div class=\"result-card-icon-wrap\">\n <i [class]=\"result.EntityIcon || result.SourceIcon || 'fa-solid fa-database'\"></i>\n </div>\n <div class=\"result-card-body\">\n <div class=\"result-card-title\" [innerHTML]=\"HighlightMatch(result.RecordName || result.Title)\"></div>\n <div class=\"result-card-entity\" [innerHTML]=\"HighlightMatch(result.EntityName)\"></div>\n @if (result.Tags && result.Tags.length > 0) {\n <div class=\"result-card-tags\">\n @for (tag of result.Tags.slice(0, 5); track tag) {\n <span class=\"result-tag-chip\" [innerHTML]=\"HighlightMatch(tag)\"></span>\n }\n @if (result.Tags.length > 5) {\n <span class=\"result-tag-more\">+{{ result.Tags.length - 5 }}</span>\n }\n </div>\n }\n </div>\n @if (ShowScores) {\n <div class=\"result-card-score\">\n <div class=\"score-circle\" [class.score-high]=\"result.Score >= 0.40\" [class.score-medium]=\"result.Score >= 0.15 && result.Score < 0.40\" [class.score-low]=\"result.Score < 0.15\">\n <span class=\"score-value\">{{ FormatScore(result.Score) }}</span>\n </div>\n </div>\n }\n <div class=\"result-card-actions\">\n <button class=\"result-open-btn\" (click)=\"OnOpenRecord(result, $event)\"\n [title]=\"result.ResultType === 'storage-file' ? 'Open file' : 'Open record'\">\n <i [class]=\"result.ResultType === 'storage-file' ? 'fa-solid fa-external-link' : 'fa-solid fa-arrow-up-right-from-square'\"></i>\n </button>\n <button class=\"result-expand-btn\" (click)=\"ToggleExpand(result, $event)\" title=\"Expand details\">\n <i class=\"fa-solid\" [class.fa-chevron-down]=\"!IsExpanded(result.ID)\"\n [class.fa-chevron-up]=\"IsExpanded(result.ID)\"></i>\n </button>\n </div>\n </div>\n\n <!-- Expanded detail -->\n @if (IsExpanded(result.ID)) {\n <div class=\"result-card-detail\">\n @if (result.Snippet) {\n <div class=\"detail-section\">\n <span class=\"detail-label\">Preview</span>\n <p class=\"detail-snippet\" [innerHTML]=\"HighlightMatch(result.Snippet)\"></p>\n </div>\n }\n <div class=\"detail-meta\">\n <div class=\"detail-meta-item\">\n <i class=\"fa-solid fa-layer-group\"></i>\n <span>{{ result.EntityName }}</span>\n </div>\n <div class=\"detail-meta-item\">\n <i class=\"fa-solid fa-fingerprint\"></i>\n <span class=\"detail-record-id\">{{ result.RecordID }}</span>\n </div>\n <!-- SR-1: Show overall score consistently -->\n <div class=\"detail-meta-item\">\n <i class=\"fa-solid fa-signal\"></i>\n <span>Relevance: {{ FormatScore(result.Score) }}</span>\n </div>\n @if (result.ScoreBreakdown) {\n <div class=\"detail-meta-item\">\n <i class=\"fa-solid fa-chart-bar\"></i>\n <span>\n @if (result.ScoreBreakdown.Vector) { Vector: {{ FormatScore(result.ScoreBreakdown.Vector) }} }\n @if (result.ScoreBreakdown.FullText) { FTS: {{ FormatScore(result.ScoreBreakdown.FullText) }} }\n </span>\n </div>\n }\n </div>\n @if (ShowTags && result.Tags.length > 0) {\n <div class=\"detail-tags\">\n @for (tag of result.Tags; track tag) {\n <span class=\"result-card-tag\" [innerHTML]=\"HighlightMatch(tag)\"></span>\n }\n </div>\n }\n @if (GetResolvedLinks(result).length > 0) {\n <div class=\"detail-links\">\n @for (link of GetResolvedLinks(result); track link.Value) {\n @if (link.Type === 'URL') {\n <a [href]=\"link.Value\" target=\"_blank\" class=\"detail-link\" (click)=\"$event.stopPropagation()\">\n <i class=\"fa-solid fa-arrow-up-right-from-square\"></i>\n {{ TruncateUrl(link.Value) }}\n </a>\n } @else if (link.Type === 'Email') {\n <a [href]=\"'mailto:' + link.Value\" class=\"detail-link detail-link-email\" (click)=\"$event.stopPropagation()\">\n <i class=\"fa-solid fa-envelope\"></i>\n {{ link.Value }}\n </a>\n }\n }\n </div>\n }\n <div class=\"detail-actions\">\n <button class=\"detail-open-btn\" (click)=\"OnOpenRecord(result, $event)\">\n @if (result.ResultType === 'storage-file') {\n <i class=\"fa-solid fa-external-link\"></i> Open File\n } @else {\n <i class=\"fa-solid fa-arrow-up-right-from-square\"></i> Open Record\n }\n </button>\n <button class=\"detail-similar-btn\" (click)=\"OnMoreLikeThis(result, $event)\">\n <i class=\"fa-solid fa-shuffle\"></i> See Similar Items\n </button>\n </div>\n </div>\n }\n </div>\n</ng-template>\n", styles: ["/* Search Results Component */\n\n:host {\n display: flex;\n flex-direction: column;\n min-height: 0;\n flex: 1;\n}\n\n.results-scroll-area {\n flex: 1;\n overflow-y: auto;\n min-height: 0;\n}\n\n.results-loading {\n display: flex;\n justify-content: center;\n padding: 3rem 1rem;\n}\n\n.results-empty {\n display: flex;\n flex-direction: column;\n align-items: center;\n padding: 3rem 1rem;\n gap: 0.5rem;\n}\n\n.results-empty-icon {\n font-size: 2rem;\n color: var(--mj-text-muted);\n margin-bottom: 0.5rem;\n}\n\n.results-empty-text {\n color: var(--mj-text-muted);\n font-size: 0.9rem;\n}\n\n.results-summary {\n padding: 0.75rem 1rem;\n color: var(--mj-text-secondary);\n font-size: 0.85rem;\n border-bottom: 1px solid var(--mj-border-subtle);\n}\n\n.results-summary-time {\n color: var(--mj-text-muted);\n}\n\n/* Group Header */\n.results-group {\n border-bottom: 1px solid var(--mj-border-subtle);\n}\n\n.results-group-header {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n padding: 0.75rem 1rem;\n cursor: pointer;\n transition: background 0.1s ease;\n}\n\n.results-group-header:hover {\n background: var(--mj-bg-surface-hover);\n}\n\n.results-group-icon {\n color: var(--mj-brand-primary);\n font-size: 0.85rem;\n}\n\n.results-group-label {\n font-weight: 600;\n font-size: 0.85rem;\n color: var(--mj-text-primary);\n flex: 1;\n}\n\n.results-group-count {\n font-size: 0.78rem;\n color: var(--mj-text-muted);\n}\n\n.results-group-chevron {\n color: var(--mj-text-muted);\n font-size: 0.7rem;\n transition: transform 0.2s ease;\n}\n\n/* Result Card \u2014 expandable */\n.result-card {\n border: 1px solid var(--mj-border-subtle);\n border-radius: 8px;\n margin: 0 0 0.35rem 1.5rem;\n overflow: hidden;\n transition: border-color 0.15s, box-shadow 0.15s;\n}\n\n.result-card:hover {\n border-color: var(--mj-border-default);\n}\n\n.result-card-expanded {\n border-color: var(--mj-brand-primary);\n box-shadow: 0 2px 8px color-mix(in srgb, var(--mj-brand-primary) 10%, transparent);\n}\n\n.result-card-main {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n padding: 0.65rem 0.85rem;\n cursor: pointer;\n transition: background 0.1s ease;\n}\n\n.result-card-main:hover {\n background: var(--mj-bg-surface-hover);\n}\n\n.result-card-icon-wrap {\n width: 2rem;\n height: 2rem;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 6px;\n background: color-mix(in srgb, var(--mj-brand-primary) 10%, var(--mj-bg-surface));\n color: var(--mj-brand-primary);\n font-size: 0.8rem;\n flex-shrink: 0;\n}\n\n.result-card-body {\n flex: 1;\n min-width: 0;\n}\n\n.result-card-title {\n color: var(--mj-text-primary);\n font-weight: 600;\n font-size: 0.85rem;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.result-card-entity {\n color: var(--mj-text-muted);\n font-size: 0.7rem;\n}\n\n.result-card-tags {\n display: flex;\n flex-wrap: wrap;\n gap: 4px;\n margin-top: 4px;\n}\n\n.result-tag-chip {\n display: inline-block;\n padding: 1px 7px;\n border-radius: 10px;\n font-size: 0.65rem;\n font-weight: 500;\n background: color-mix(in srgb, var(--mj-brand-primary) 10%, var(--mj-bg-surface));\n color: var(--mj-brand-primary);\n border: 1px solid color-mix(in srgb, var(--mj-brand-primary) 20%, var(--mj-border-default));\n}\n\n.result-tag-more {\n display: inline-block;\n padding: 1px 6px;\n font-size: 0.6rem;\n color: var(--mj-text-muted);\n}\n\n.result-card-actions {\n display: flex;\n align-items: center;\n gap: 4px;\n flex-shrink: 0;\n}\n\n.result-open-btn,\n.result-expand-btn {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n border: 1px solid var(--mj-border-default);\n border-radius: 6px;\n background: transparent;\n color: var(--mj-text-muted);\n font-size: 11px;\n cursor: pointer;\n transition: all 0.15s;\n}\n\n.result-open-btn:hover {\n background: color-mix(in srgb, var(--mj-brand-primary) 10%, var(--mj-bg-surface));\n border-color: var(--mj-brand-primary);\n color: var(--mj-brand-primary);\n}\n\n.result-expand-btn:hover {\n background: var(--mj-bg-surface-hover);\n color: var(--mj-text-primary);\n}\n\n/* Expanded detail panel */\n.result-card-detail {\n padding: 0.75rem 1rem 0.85rem 3.5rem;\n border-top: 1px solid var(--mj-border-subtle);\n background: var(--mj-bg-surface-card);\n}\n\n.detail-section {\n margin-bottom: 0.65rem;\n}\n\n.detail-label {\n font-size: 0.7rem;\n font-weight: 600;\n color: var(--mj-text-muted);\n text-transform: uppercase;\n letter-spacing: 0.04em;\n margin-bottom: 0.25rem;\n display: block;\n}\n\n.detail-snippet {\n margin: 0;\n font-size: 0.82rem;\n color: var(--mj-text-secondary);\n line-height: 1.5;\n}\n\n.detail-meta {\n display: flex;\n flex-wrap: wrap;\n gap: 0.75rem;\n margin-bottom: 0.65rem;\n}\n\n.detail-meta-item {\n display: flex;\n align-items: center;\n gap: 5px;\n font-size: 0.75rem;\n color: var(--mj-text-muted);\n}\n\n.detail-meta-item i {\n font-size: 0.65rem;\n}\n\n.detail-record-id {\n font-family: monospace;\n font-size: 0.7rem;\n max-width: 180px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.detail-tags {\n display: flex;\n flex-wrap: wrap;\n gap: 4px;\n margin-bottom: 0.65rem;\n}\n\n.detail-links {\n display: flex;\n flex-wrap: wrap;\n gap: 6px;\n margin-bottom: 8px;\n}\n\n.detail-link {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n padding: 3px 8px;\n border-radius: 6px;\n font-size: 0.72rem;\n color: var(--mj-text-link);\n background: color-mix(in srgb, var(--mj-brand-primary) 5%, var(--mj-bg-surface));\n border: 1px solid color-mix(in srgb, var(--mj-brand-primary) 15%, var(--mj-border-default));\n text-decoration: none;\n transition: background 0.15s ease;\n}\n\n.detail-link:hover {\n background: color-mix(in srgb, var(--mj-brand-primary) 12%, var(--mj-bg-surface));\n text-decoration: underline;\n}\n\n.detail-link i {\n font-size: 0.6rem;\n}\n\n.detail-actions {\n display: flex;\n gap: 8px;\n}\n\n.detail-open-btn {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n padding: 6px 14px;\n border: 1px solid var(--mj-brand-primary);\n border-radius: 6px;\n background: transparent;\n color: var(--mj-brand-primary);\n font-size: 0.78rem;\n font-weight: 500;\n cursor: pointer;\n transition: all 0.15s;\n}\n\n.detail-open-btn:hover {\n background: color-mix(in srgb, var(--mj-brand-primary) 10%, var(--mj-bg-surface));\n}\n\n.detail-similar-btn {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n padding: 6px 14px;\n border: 1px solid var(--mj-border-default);\n border-radius: 6px;\n background: transparent;\n color: var(--mj-text-secondary);\n font-size: 0.78rem;\n font-weight: 500;\n cursor: pointer;\n transition: all 0.15s;\n}\n\n.detail-similar-btn:hover {\n border-color: var(--mj-brand-primary);\n color: var(--mj-brand-primary);\n background: color-mix(in srgb, var(--mj-brand-primary) 5%, var(--mj-bg-surface));\n}\n\n.result-card-snippet {\n color: var(--mj-text-secondary);\n font-size: 0.82rem;\n line-height: 1.4;\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n overflow: hidden;\n}\n\n.result-card-tags {\n display: flex;\n flex-wrap: wrap;\n gap: 0.25rem;\n margin-top: 0.35rem;\n}\n\n.result-card-tag {\n display: inline-block;\n padding: 0.1rem 0.4rem;\n border-radius: 4px;\n background: var(--mj-bg-surface-sunken);\n color: var(--mj-text-muted);\n font-size: 0.7rem;\n}\n\n.result-card-tag-more {\n color: var(--mj-brand-primary);\n}\n\n.result-card-score {\n flex-shrink: 0;\n margin-top: 0.2rem;\n}\n\n.score-circle {\n width: 2.5rem;\n height: 2.5rem;\n border-radius: 50%;\n display: flex;\n align-items: center;\n justify-content: center;\n /* Default: neutral gray */\n background: color-mix(in srgb, var(--mj-text-muted) 10%, var(--mj-bg-surface));\n border: 2px solid color-mix(in srgb, var(--mj-text-muted) 25%, var(--mj-bg-surface));\n}\n.score-circle.score-high {\n background: color-mix(in srgb, var(--mj-status-success) 15%, var(--mj-bg-surface));\n border-color: color-mix(in srgb, var(--mj-status-success) 40%, var(--mj-bg-surface));\n}\n.score-circle.score-medium {\n background: color-mix(in srgb, var(--mj-status-warning) 15%, var(--mj-bg-surface));\n border-color: color-mix(in srgb, var(--mj-status-warning) 40%, var(--mj-bg-surface));\n}\n.score-circle.score-low {\n background: color-mix(in srgb, var(--mj-text-muted) 10%, var(--mj-bg-surface));\n border-color: color-mix(in srgb, var(--mj-text-muted) 25%, var(--mj-bg-surface));\n}\n\n.score-value {\n font-size: 0.7rem;\n font-weight: 700;\n color: var(--mj-text-secondary);\n}\n.score-high .score-value {\n color: var(--mj-status-success-text, var(--mj-status-success));\n}\n.score-medium .score-value {\n color: var(--mj-status-warning-text, var(--mj-status-warning));\n}\n.score-low .score-value {\n color: var(--mj-text-muted);\n}\n\n/* Show More Button */\n.results-show-more {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n width: 100%;\n padding: 0.5rem 1rem;\n border: none;\n background: transparent;\n color: var(--mj-brand-primary);\n font-size: 0.82rem;\n cursor: pointer;\n justify-content: center;\n transition: background 0.1s ease;\n}\n\n.results-show-more:hover {\n background: var(--mj-bg-surface-hover);\n}\n\n/* Google-style Pagination \u2014 pinned at bottom */\n.results-pager {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 4px;\n padding: 0.75rem 0;\n flex-shrink: 0;\n border-top: 1px solid var(--mj-border-subtle);\n background: var(--mj-bg-surface);\n}\n\n.pager-btn {\n display: flex;\n align-items: center;\n justify-content: center;\n min-width: 32px;\n height: 32px;\n border: 1px solid var(--mj-border-default);\n border-radius: 8px;\n background: var(--mj-bg-surface);\n color: var(--mj-text-secondary);\n font-size: 0.82rem;\n font-weight: 500;\n cursor: pointer;\n transition: all 0.15s ease;\n padding: 0 6px;\n}\n\n.pager-btn:hover:not(:disabled) {\n background: var(--mj-bg-surface-hover);\n border-color: var(--mj-brand-primary);\n color: var(--mj-brand-primary);\n}\n\n.pager-btn:disabled {\n opacity: 0.35;\n cursor: default;\n}\n\n.pager-active {\n background: var(--mj-brand-primary);\n border-color: var(--mj-brand-primary);\n color: var(--mj-text-inverse);\n}\n\n.pager-active:hover:not(:disabled) {\n background: var(--mj-brand-primary-hover);\n border-color: var(--mj-brand-primary-hover);\n color: var(--mj-text-inverse);\n}\n\n.pager-ellipsis {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 28px;\n color: var(--mj-text-muted);\n font-size: 0.8rem;\n}\n\n.pager-prev i,\n.pager-next i {\n font-size: 0.7rem;\n}\n\n/* Search text highlighting */\n:host ::ng-deep .search-highlight,\n.search-highlight {\n background: color-mix(in srgb, var(--mj-status-warning) 35%, transparent);\n color: inherit;\n padding: 1px 2px;\n border-radius: 2px;\n}\n"] }]
703
+ args: [{ standalone: false, selector: 'mj-search-results', template: "@if (IsLoading) {\n <div class=\"results-loading\">\n <mj-loading text=\"Searching...\" size=\"medium\"></mj-loading>\n </div>\n} @else if ((DisplayMode === 'flat' ? FlatResults.length : ResultGroups.length) === 0 && TotalCount === 0) {\n <div class=\"results-empty\">\n <i class=\"fa-solid fa-search results-empty-icon\"></i>\n <p class=\"results-empty-text\">No results to display</p>\n </div>\n} @else {\n <!-- Results summary -->\n @if (ShowSummary) {\n <div class=\"results-summary\">\n <span>{{ TotalCount }} results</span>\n @if (ElapsedMs > 0) {\n <span class=\"results-summary-time\"> in {{ FormatTime(ElapsedMs) }}</span>\n }\n </div>\n }\n\n @if (DisplayMode === 'flat') {\n <!-- \u2550\u2550\u2550\u2550\u2550\u2550 FLAT BLENDED LIST (sorted by score) \u2550\u2550\u2550\u2550\u2550\u2550 -->\n <div class=\"results-scroll-area\">\n @for (result of PagedResults; track result.ID) {\n <ng-container *ngTemplateOutlet=\"resultCardTpl; context: { $implicit: result }\"></ng-container>\n }\n </div>\n\n <!-- Google-style pagination (sticky at bottom) -->\n @if (TotalPages > 1) {\n <div class=\"results-pager\">\n <button class=\"pager-btn pager-prev\" [disabled]=\"CurrentPage === 1\"\n (click)=\"GoToPage(CurrentPage - 1)\" aria-label=\"Previous page\">\n <i class=\"fa-solid fa-chevron-left\"></i>\n </button>\n @for (p of PageNumbers; track $index) {\n @if (p === -1) {\n <span class=\"pager-ellipsis\">...</span>\n } @else {\n <button class=\"pager-btn pager-num\" [class.pager-active]=\"p === CurrentPage\"\n (click)=\"GoToPage(p)\">{{ p }}</button>\n }\n }\n <button class=\"pager-btn pager-next\" [disabled]=\"CurrentPage === TotalPages\"\n (click)=\"GoToPage(CurrentPage + 1)\" aria-label=\"Next page\">\n <i class=\"fa-solid fa-chevron-right\"></i>\n </button>\n </div>\n }\n } @else {\n <!-- \u2550\u2550\u2550\u2550\u2550\u2550 GROUPED BY SOURCE TYPE \u2550\u2550\u2550\u2550\u2550\u2550 -->\n @for (group of ResultGroups; track group.SourceType) {\n <div class=\"results-group\">\n <div class=\"results-group-header\" (click)=\"ToggleGroup(group.SourceType)\">\n @if (ShowSourceIcons) {\n <i [class]=\"group.Icon + ' results-group-icon'\"></i>\n }\n <span class=\"results-group-label\">{{ group.Label }}</span>\n <span class=\"results-group-count\">({{ group.TotalCount }})</span>\n <i class=\"fa-solid results-group-chevron\"\n [class.fa-chevron-down]=\"!IsGroupExpanded(group.SourceType)\"\n [class.fa-chevron-up]=\"IsGroupExpanded(group.SourceType)\"></i>\n </div>\n\n @for (result of GetVisibleResults(group); track result.ID) {\n <ng-container *ngTemplateOutlet=\"resultCardTpl; context: { $implicit: result }\"></ng-container>\n }\n\n @if (HasMoreResults(group)) {\n <button class=\"results-show-more\" (click)=\"ToggleGroup(group.SourceType)\">\n <i class=\"fa-solid fa-chevron-down\"></i>\n Show {{ GetHiddenCount(group) }} more results\n </button>\n }\n </div>\n }\n }\n}\n\n<!-- \u2550\u2550\u2550 Shared result card template \u2550\u2550\u2550 -->\n<ng-template #resultCardTpl let-result>\n <div class=\"result-card\" [class.result-card-expanded]=\"IsExpanded(result.ID)\">\n <!-- Main row -->\n <div class=\"result-card-main\" (click)=\"ToggleExpand(result)\">\n <div class=\"result-card-icon-wrap\">\n <i [class]=\"result.EntityIcon || result.SourceIcon || 'fa-solid fa-database'\"></i>\n </div>\n <div class=\"result-card-body\">\n <div class=\"result-card-title\" [innerHTML]=\"HighlightMatch(result.RecordName || result.Title)\"></div>\n <div class=\"result-card-entity\" [innerHTML]=\"HighlightMatch(result.EntityName)\"></div>\n @if (result.Tags && result.Tags.length > 0) {\n <div class=\"result-card-tags\">\n @for (tag of result.Tags.slice(0, 5); track tag) {\n <span class=\"result-tag-chip\" [innerHTML]=\"HighlightMatch(tag)\"></span>\n }\n @if (result.Tags.length > 5) {\n <span class=\"result-tag-more\">+{{ result.Tags.length - 5 }}</span>\n }\n </div>\n }\n </div>\n @if (ShowScores) {\n <div class=\"result-card-score\">\n <div class=\"score-circle\" [class.score-high]=\"result.Score >= 0.40\" [class.score-medium]=\"result.Score >= 0.15 && result.Score < 0.40\" [class.score-low]=\"result.Score < 0.15\">\n <span class=\"score-value\">{{ FormatScore(result.Score) }}</span>\n </div>\n </div>\n }\n <div class=\"result-card-actions\">\n <button class=\"result-open-btn\" (click)=\"OnOpenRecord(result, $event)\"\n [title]=\"result.ResultType === 'storage-file' ? 'Open file' : 'Open record'\"\n [attr.aria-label]=\"result.ResultType === 'storage-file' ? 'Open file' : 'Open record'\">\n <i [class]=\"result.ResultType === 'storage-file' ? 'fa-solid fa-external-link' : 'fa-solid fa-arrow-up-right-from-square'\"></i>\n </button>\n <button class=\"result-expand-btn\" (click)=\"ToggleExpand(result, $event)\" title=\"Expand details\" aria-label=\"Expand details\">\n <i class=\"fa-solid\" [class.fa-chevron-down]=\"!IsExpanded(result.ID)\"\n [class.fa-chevron-up]=\"IsExpanded(result.ID)\"></i>\n </button>\n </div>\n </div>\n\n <!-- Expanded detail -->\n @if (IsExpanded(result.ID)) {\n <div class=\"result-card-detail\">\n @if (result.Snippet) {\n <div class=\"detail-section\">\n <span class=\"detail-label\">Preview</span>\n <p class=\"detail-snippet\" [innerHTML]=\"HighlightMatch(result.Snippet)\"></p>\n </div>\n }\n <div class=\"detail-meta\">\n <div class=\"detail-meta-item\">\n <i class=\"fa-solid fa-layer-group\"></i>\n <span>{{ result.EntityName }}</span>\n </div>\n <div class=\"detail-meta-item\">\n <i class=\"fa-solid fa-fingerprint\"></i>\n <span class=\"detail-record-id\">{{ result.RecordID }}</span>\n </div>\n <!-- SR-1: Show overall score consistently -->\n <div class=\"detail-meta-item\">\n <i class=\"fa-solid fa-signal\"></i>\n <span>Relevance: {{ FormatScore(result.Score) }}</span>\n </div>\n @if (result.ScoreBreakdown) {\n <div class=\"detail-meta-item\">\n <i class=\"fa-solid fa-chart-bar\"></i>\n <span>\n @if (result.ScoreBreakdown.Vector) { Vector: {{ FormatScore(result.ScoreBreakdown.Vector) }} }\n @if (result.ScoreBreakdown.FullText) { FTS: {{ FormatScore(result.ScoreBreakdown.FullText) }} }\n </span>\n </div>\n }\n </div>\n @if (ShowTags && result.Tags.length > 0) {\n <div class=\"detail-tags\">\n @for (tag of result.Tags; track tag) {\n <span class=\"result-card-tag\" [innerHTML]=\"HighlightMatch(tag)\"></span>\n }\n </div>\n }\n @if (GetResolvedLinks(result).length > 0) {\n <div class=\"detail-links\">\n @for (link of GetResolvedLinks(result); track link.Value) {\n @if (link.Type === 'URL') {\n <a [href]=\"link.Value\" target=\"_blank\" class=\"detail-link\" (click)=\"$event.stopPropagation()\">\n <i class=\"fa-solid fa-arrow-up-right-from-square\"></i>\n {{ TruncateUrl(link.Value) }}\n </a>\n } @else if (link.Type === 'Email') {\n <a [href]=\"'mailto:' + link.Value\" class=\"detail-link detail-link-email\" (click)=\"$event.stopPropagation()\">\n <i class=\"fa-solid fa-envelope\"></i>\n {{ link.Value }}\n </a>\n }\n }\n </div>\n }\n <div class=\"detail-actions\">\n <button class=\"detail-open-btn\" (click)=\"OnOpenRecord(result, $event)\">\n @if (result.ResultType === 'storage-file') {\n <i class=\"fa-solid fa-external-link\"></i> Open File\n } @else {\n <i class=\"fa-solid fa-arrow-up-right-from-square\"></i> Open Record\n }\n </button>\n <button class=\"detail-similar-btn\" (click)=\"OnMoreLikeThis(result, $event)\">\n <i class=\"fa-solid fa-shuffle\"></i> See Similar Items\n </button>\n </div>\n </div>\n }\n </div>\n</ng-template>\n", styles: ["/* Search Results Component */\n\n:host {\n display: flex;\n flex-direction: column;\n min-height: 0;\n flex: 1;\n}\n\n.results-scroll-area {\n flex: 1;\n overflow-y: auto;\n min-height: 0;\n}\n\n.results-loading {\n display: flex;\n justify-content: center;\n padding: 3rem 1rem;\n}\n\n.results-empty {\n display: flex;\n flex-direction: column;\n align-items: center;\n padding: 3rem 1rem;\n gap: 0.5rem;\n}\n\n.results-empty-icon {\n font-size: 2rem;\n color: var(--mj-text-muted);\n margin-bottom: 0.5rem;\n}\n\n.results-empty-text {\n color: var(--mj-text-muted);\n font-size: 0.9rem;\n}\n\n.results-summary {\n padding: 0.75rem 1rem;\n color: var(--mj-text-secondary);\n font-size: 0.85rem;\n border-bottom: 1px solid var(--mj-border-subtle);\n}\n\n.results-summary-time {\n color: var(--mj-text-muted);\n}\n\n/* Group Header */\n.results-group {\n border-bottom: 1px solid var(--mj-border-subtle);\n}\n\n.results-group-header {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n padding: 0.75rem 1rem;\n cursor: pointer;\n transition: background 0.1s ease;\n}\n\n.results-group-header:hover {\n background: var(--mj-bg-surface-hover);\n}\n\n.results-group-icon {\n color: var(--mj-brand-primary);\n font-size: 0.85rem;\n}\n\n.results-group-label {\n font-weight: 600;\n font-size: 0.85rem;\n color: var(--mj-text-primary);\n flex: 1;\n}\n\n.results-group-count {\n font-size: 0.78rem;\n color: var(--mj-text-muted);\n}\n\n.results-group-chevron {\n color: var(--mj-text-muted);\n font-size: 0.7rem;\n transition: transform 0.2s ease;\n}\n\n/* Result Card \u2014 expandable */\n.result-card {\n border: 1px solid var(--mj-border-subtle);\n border-radius: 8px;\n margin: 0 0 0.35rem 1.5rem;\n overflow: hidden;\n transition: border-color 0.15s, box-shadow 0.15s;\n}\n\n.result-card:hover {\n border-color: var(--mj-border-default);\n}\n\n.result-card-expanded {\n border-color: var(--mj-brand-primary);\n box-shadow: 0 2px 8px color-mix(in srgb, var(--mj-brand-primary) 10%, transparent);\n}\n\n.result-card-main {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n padding: 0.65rem 0.85rem;\n cursor: pointer;\n transition: background 0.1s ease;\n}\n\n.result-card-main:hover {\n background: var(--mj-bg-surface-hover);\n}\n\n.result-card-icon-wrap {\n width: 2rem;\n height: 2rem;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 6px;\n background: color-mix(in srgb, var(--mj-brand-primary) 10%, var(--mj-bg-surface));\n color: var(--mj-brand-primary);\n font-size: 0.8rem;\n flex-shrink: 0;\n}\n\n.result-card-body {\n flex: 1;\n min-width: 0;\n}\n\n.result-card-title {\n color: var(--mj-text-primary);\n font-weight: 600;\n font-size: 0.85rem;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.result-card-entity {\n color: var(--mj-text-muted);\n font-size: 0.7rem;\n}\n\n.result-card-tags {\n display: flex;\n flex-wrap: wrap;\n gap: 4px;\n margin-top: 4px;\n}\n\n.result-tag-chip {\n display: inline-block;\n padding: 1px 7px;\n border-radius: 10px;\n font-size: 0.65rem;\n font-weight: 500;\n background: color-mix(in srgb, var(--mj-brand-primary) 10%, var(--mj-bg-surface));\n color: var(--mj-brand-primary);\n border: 1px solid color-mix(in srgb, var(--mj-brand-primary) 20%, var(--mj-border-default));\n}\n\n.result-tag-more {\n display: inline-block;\n padding: 1px 6px;\n font-size: 0.6rem;\n color: var(--mj-text-muted);\n}\n\n.result-card-actions {\n display: flex;\n align-items: center;\n gap: 4px;\n flex-shrink: 0;\n}\n\n.result-open-btn,\n.result-expand-btn {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n border: 1px solid var(--mj-border-default);\n border-radius: 6px;\n background: transparent;\n color: var(--mj-text-muted);\n font-size: 11px;\n cursor: pointer;\n transition: all 0.15s;\n}\n\n.result-open-btn:hover {\n background: color-mix(in srgb, var(--mj-brand-primary) 10%, var(--mj-bg-surface));\n border-color: var(--mj-brand-primary);\n color: var(--mj-brand-primary);\n}\n\n.result-expand-btn:hover {\n background: var(--mj-bg-surface-hover);\n color: var(--mj-text-primary);\n}\n\n/* Expanded detail panel */\n.result-card-detail {\n padding: 0.75rem 1rem 0.85rem 3.5rem;\n border-top: 1px solid var(--mj-border-subtle);\n background: var(--mj-bg-surface-card);\n}\n\n.detail-section {\n margin-bottom: 0.65rem;\n}\n\n.detail-label {\n font-size: 0.7rem;\n font-weight: 600;\n color: var(--mj-text-muted);\n text-transform: uppercase;\n letter-spacing: 0.04em;\n margin-bottom: 0.25rem;\n display: block;\n}\n\n.detail-snippet {\n margin: 0;\n font-size: 0.82rem;\n color: var(--mj-text-secondary);\n line-height: 1.5;\n}\n\n.detail-meta {\n display: flex;\n flex-wrap: wrap;\n gap: 0.75rem;\n margin-bottom: 0.65rem;\n}\n\n.detail-meta-item {\n display: flex;\n align-items: center;\n gap: 5px;\n font-size: 0.75rem;\n color: var(--mj-text-muted);\n}\n\n.detail-meta-item i {\n font-size: 0.65rem;\n}\n\n.detail-record-id {\n font-family: monospace;\n font-size: 0.7rem;\n max-width: 180px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.detail-tags {\n display: flex;\n flex-wrap: wrap;\n gap: 4px;\n margin-bottom: 0.65rem;\n}\n\n.detail-links {\n display: flex;\n flex-wrap: wrap;\n gap: 6px;\n margin-bottom: 8px;\n}\n\n.detail-link {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n padding: 3px 8px;\n border-radius: 6px;\n font-size: 0.72rem;\n color: var(--mj-text-link);\n background: color-mix(in srgb, var(--mj-brand-primary) 5%, var(--mj-bg-surface));\n border: 1px solid color-mix(in srgb, var(--mj-brand-primary) 15%, var(--mj-border-default));\n text-decoration: none;\n transition: background 0.15s ease;\n}\n\n.detail-link:hover {\n background: color-mix(in srgb, var(--mj-brand-primary) 12%, var(--mj-bg-surface));\n text-decoration: underline;\n}\n\n.detail-link i {\n font-size: 0.6rem;\n}\n\n.detail-actions {\n display: flex;\n gap: 8px;\n}\n\n.detail-open-btn {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n padding: 6px 14px;\n border: 1px solid var(--mj-brand-primary);\n border-radius: 6px;\n background: transparent;\n color: var(--mj-brand-primary);\n font-size: 0.78rem;\n font-weight: 500;\n cursor: pointer;\n transition: all 0.15s;\n}\n\n.detail-open-btn:hover {\n background: color-mix(in srgb, var(--mj-brand-primary) 10%, var(--mj-bg-surface));\n}\n\n.detail-similar-btn {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n padding: 6px 14px;\n border: 1px solid var(--mj-border-default);\n border-radius: 6px;\n background: transparent;\n color: var(--mj-text-secondary);\n font-size: 0.78rem;\n font-weight: 500;\n cursor: pointer;\n transition: all 0.15s;\n}\n\n.detail-similar-btn:hover {\n border-color: var(--mj-brand-primary);\n color: var(--mj-brand-primary);\n background: color-mix(in srgb, var(--mj-brand-primary) 5%, var(--mj-bg-surface));\n}\n\n.result-card-snippet {\n color: var(--mj-text-secondary);\n font-size: 0.82rem;\n line-height: 1.4;\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n overflow: hidden;\n}\n\n.result-card-tags {\n display: flex;\n flex-wrap: wrap;\n gap: 0.25rem;\n margin-top: 0.35rem;\n}\n\n.result-card-tag {\n display: inline-block;\n padding: 0.1rem 0.4rem;\n border-radius: 4px;\n background: var(--mj-bg-surface-sunken);\n color: var(--mj-text-muted);\n font-size: 0.7rem;\n}\n\n.result-card-tag-more {\n color: var(--mj-brand-primary);\n}\n\n.result-card-score {\n flex-shrink: 0;\n margin-top: 0.2rem;\n}\n\n.score-circle {\n width: 2.5rem;\n height: 2.5rem;\n border-radius: 50%;\n display: flex;\n align-items: center;\n justify-content: center;\n /* Default: neutral gray */\n background: color-mix(in srgb, var(--mj-text-muted) 10%, var(--mj-bg-surface));\n border: 2px solid color-mix(in srgb, var(--mj-text-muted) 25%, var(--mj-bg-surface));\n}\n.score-circle.score-high {\n background: color-mix(in srgb, var(--mj-status-success) 15%, var(--mj-bg-surface));\n border-color: color-mix(in srgb, var(--mj-status-success) 40%, var(--mj-bg-surface));\n}\n.score-circle.score-medium {\n background: color-mix(in srgb, var(--mj-status-warning) 15%, var(--mj-bg-surface));\n border-color: color-mix(in srgb, var(--mj-status-warning) 40%, var(--mj-bg-surface));\n}\n.score-circle.score-low {\n background: color-mix(in srgb, var(--mj-text-muted) 10%, var(--mj-bg-surface));\n border-color: color-mix(in srgb, var(--mj-text-muted) 25%, var(--mj-bg-surface));\n}\n\n.score-value {\n font-size: 0.7rem;\n font-weight: 700;\n color: var(--mj-text-secondary);\n}\n.score-high .score-value {\n color: var(--mj-status-success-text, var(--mj-status-success));\n}\n.score-medium .score-value {\n color: var(--mj-status-warning-text, var(--mj-status-warning));\n}\n.score-low .score-value {\n color: var(--mj-text-muted);\n}\n\n/* Show More Button */\n.results-show-more {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n width: 100%;\n padding: 0.5rem 1rem;\n border: none;\n background: transparent;\n color: var(--mj-brand-primary);\n font-size: 0.82rem;\n cursor: pointer;\n justify-content: center;\n transition: background 0.1s ease;\n}\n\n.results-show-more:hover {\n background: var(--mj-bg-surface-hover);\n}\n\n/* Google-style Pagination \u2014 pinned at bottom */\n.results-pager {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 4px;\n padding: 0.75rem 0;\n flex-shrink: 0;\n border-top: 1px solid var(--mj-border-subtle);\n background: var(--mj-bg-surface);\n}\n\n.pager-btn {\n display: flex;\n align-items: center;\n justify-content: center;\n min-width: 32px;\n height: 32px;\n border: 1px solid var(--mj-border-default);\n border-radius: 8px;\n background: var(--mj-bg-surface);\n color: var(--mj-text-secondary);\n font-size: 0.82rem;\n font-weight: 500;\n cursor: pointer;\n transition: all 0.15s ease;\n padding: 0 6px;\n}\n\n.pager-btn:hover:not(:disabled) {\n background: var(--mj-bg-surface-hover);\n border-color: var(--mj-brand-primary);\n color: var(--mj-brand-primary);\n}\n\n.pager-btn:disabled {\n opacity: 0.35;\n cursor: default;\n}\n\n.pager-active {\n background: var(--mj-brand-primary);\n border-color: var(--mj-brand-primary);\n color: var(--mj-text-inverse);\n}\n\n.pager-active:hover:not(:disabled) {\n background: var(--mj-brand-primary-hover);\n border-color: var(--mj-brand-primary-hover);\n color: var(--mj-text-inverse);\n}\n\n.pager-ellipsis {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 28px;\n color: var(--mj-text-muted);\n font-size: 0.8rem;\n}\n\n.pager-prev i,\n.pager-next i {\n font-size: 0.7rem;\n}\n\n/* Search text highlighting */\n:host ::ng-deep .search-highlight,\n.search-highlight {\n background: color-mix(in srgb, var(--mj-status-warning) 35%, transparent);\n color: inherit;\n padding: 1px 2px;\n border-radius: 2px;\n}\n"] }]
703
704
  }], null, { ResultGroups: [{
704
705
  type: Input
705
706
  }], FlatResults: [{
@@ -1 +1 @@
1
- {"version":3,"file":"search-results.component.js","sourceRoot":"","sources":["../../src/lib/search-results.component.ts","../../src/lib/search-results.component.html"],"names":[],"mappings":"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","sourcesContent":["/**\n * @fileoverview Search Results Component\n *\n * Displays search results grouped by source type with expandable sections.\n * Used both within the search overlay and as a standalone results view\n * in the Knowledge Hub search dashboard.\n */\n\nimport {\n Component,\n EventEmitter,\n Input,\n Output,\n ChangeDetectorRef,\n inject\n} from '@angular/core';\nimport { SearchResultItem, SearchResultGroup, SearchResultSelectedEvent } from './search-types';\nimport { Metadata } from '@memberjunction/core';\nimport { EscapeHTML } from '@memberjunction/global';\n\n@Component({\n standalone: false,\n selector: 'mj-search-results',\n templateUrl: './search-results.component.html',\n styleUrls: ['./search-results.component.css']\n})\nexport class SearchResultsComponent {\n private cdr = inject(ChangeDetectorRef);\n\n /** Grouped search results to display (used when DisplayMode is 'grouped') */\n @Input() ResultGroups: SearchResultGroup[] = [];\n\n /** Flat list of all results sorted by score (used when DisplayMode is 'flat') */\n @Input() FlatResults: SearchResultItem[] = [];\n\n /** Display mode: 'flat' for blended list sorted by score, 'grouped' for source-type groups */\n @Input() DisplayMode: 'flat' | 'grouped' = 'flat';\n\n /** Whether results are currently loading */\n @Input() IsLoading = false;\n\n /** Total result count */\n @Input() TotalCount = 0;\n\n /** Elapsed search time in ms */\n @Input() ElapsedMs = 0;\n\n /** Maximum results per group before \"Show more\" (grouped mode only) */\n @Input() MaxPerGroup = 5;\n\n /** Results per page in flat mode */\n @Input() PageSize = 10;\n\n /** Whether to show the results summary line (count + time). Set false when parent provides its own header. */\n @Input() ShowSummary = true;\n\n /** Whether to show score badges */\n @Input() ShowScores = true;\n\n /** Whether to show tags */\n @Input() ShowTags = true;\n\n /** Whether to show source type icons */\n @Input() ShowSourceIcons = true;\n\n /** Text to highlight within result cards (e.g. from a client-side filter) */\n @Input() HighlightText = '';\n\n @Output() ResultSelected = new EventEmitter<SearchResultSelectedEvent>();\n\n /** Emitted when user clicks \"Open Record\" — parent handles navigation */\n @Output() OpenRecordRequested = new EventEmitter<SearchResultItem>();\n\n /** Emitted when user clicks \"See Similar Items\" — parent runs a \"more like this\" search */\n @Output() MoreLikeThisRequested = new EventEmitter<SearchResultItem>();\n\n /** Current page (1-based) for flat mode pagination */\n public CurrentPage = 1;\n\n /** Tracks which groups are expanded */\n public ExpandedGroups = new Set<string>();\n\n /** Set of expanded result card IDs — multiple can be open simultaneously */\n public ExpandedResultIDs = new Set<string>();\n\n // ── Flat mode pagination ──\n\n /** Get results for the current page in flat mode */\n public get PagedResults(): SearchResultItem[] {\n const start = (this.CurrentPage - 1) * this.PageSize;\n return this.FlatResults.slice(start, start + this.PageSize);\n }\n\n /** Total number of pages */\n public get TotalPages(): number {\n return Math.ceil(this.FlatResults.length / this.PageSize);\n }\n\n /** Page numbers to display in the pager */\n public get PageNumbers(): number[] {\n const total = this.TotalPages;\n if (total <= 7) return Array.from({ length: total }, (_, i) => i + 1);\n\n const pages: number[] = [1];\n const start = Math.max(2, this.CurrentPage - 2);\n const end = Math.min(total - 1, this.CurrentPage + 2);\n if (start > 2) pages.push(-1); // ellipsis marker\n for (let i = start; i <= end; i++) pages.push(i);\n if (end < total - 1) pages.push(-1); // ellipsis marker\n pages.push(total);\n return pages;\n }\n\n /** Navigate to a specific page */\n public GoToPage(page: number): void {\n if (page < 1 || page > this.TotalPages) return;\n this.CurrentPage = page;\n this.cdr.detectChanges();\n }\n\n // ── Grouped mode ──\n\n /** Toggle group expansion */\n public ToggleGroup(sourceType: string): void {\n if (this.ExpandedGroups.has(sourceType)) {\n this.ExpandedGroups.delete(sourceType);\n } else {\n this.ExpandedGroups.add(sourceType);\n }\n this.cdr.detectChanges();\n }\n\n /** Whether a group is expanded */\n public IsGroupExpanded(sourceType: string): boolean {\n return this.ExpandedGroups.has(sourceType);\n }\n\n /** Get visible results for a group (respecting max per group unless expanded) */\n public GetVisibleResults(group: SearchResultGroup): SearchResultItem[] {\n if (this.IsGroupExpanded(group.SourceType)) {\n return group.Results;\n }\n return group.Results.slice(0, this.MaxPerGroup);\n }\n\n /** Whether a group has more results than shown */\n public HasMoreResults(group: SearchResultGroup): boolean {\n return group.Results.length > this.MaxPerGroup && !this.IsGroupExpanded(group.SourceType);\n }\n\n /** Get the count of hidden results */\n public GetHiddenCount(group: SearchResultGroup): number {\n return group.Results.length - this.MaxPerGroup;\n }\n\n /** Handle clicking a result */\n public OnResultClick(result: SearchResultItem, event: MouseEvent): void {\n this.ResultSelected.emit({\n Result: result,\n OpenInNewTab: event.metaKey || event.ctrlKey\n });\n }\n\n /** Toggle expand/collapse of a result card — multiple can be open */\n public ToggleExpand(result: SearchResultItem, event?: MouseEvent): void {\n if (event) event.stopPropagation();\n if (this.ExpandedResultIDs.has(result.ID)) {\n this.ExpandedResultIDs.delete(result.ID);\n } else {\n this.ExpandedResultIDs.add(result.ID);\n }\n this.cdr.detectChanges();\n }\n\n /** Check if a result card is expanded */\n public IsExpanded(resultID: string): boolean {\n return this.ExpandedResultIDs.has(resultID);\n }\n\n /** Handle \"Open Record\" button click */\n public OnOpenRecord(result: SearchResultItem, event: MouseEvent): void {\n event.stopPropagation();\n this.OpenRecordRequested.emit(result);\n }\n\n /** Handle \"See Similar Items\" button click — emits the result for a \"more like this\" search */\n public OnMoreLikeThis(result: SearchResultItem, event: MouseEvent): void {\n event.stopPropagation();\n this.MoreLikeThisRequested.emit(result);\n }\n\n /**\n * Wrap substrings matching HighlightText in <mark> tags for visual emphasis.\n * The input is regex-escaped so user text is treated as a literal string.\n */\n public HighlightMatch(text: string): string {\n if (!text) return text;\n const safeText = EscapeHTML(text);\n if (!this.HighlightText) return safeText;\n\n const safeHighlightText = EscapeHTML(this.HighlightText);\n const escaped = safeHighlightText.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n const regex = new RegExp(`(${escaped})`, 'gi');\n return safeText.replace(regex, '<mark class=\"search-highlight\">$1</mark>');\n }\n\n /** Format a score as a percentage */\n public FormatScore(score: number): string {\n return `${Math.round(score * 100)}%`;\n }\n\n /** Format elapsed time */\n public FormatTime(ms: number): string {\n if (ms < 1000) return `${ms}ms`;\n return `${(ms / 1000).toFixed(1)}s`;\n }\n\n /** Resolved link cache per result ID */\n private resolvedLinksCache = new Map<string, { Value: string; Label: string; Type: 'URL' | 'Email' }[]>();\n\n /**\n * Resolve URL and Email fields from the result's RawMetadata using entity ExtendedType metadata.\n * Returns clickable link entries with proper type classification.\n */\n public GetResolvedLinks(result: SearchResultItem): { Value: string; Label: string; Type: 'URL' | 'Email' }[] {\n if (this.resolvedLinksCache.has(result.ID)) {\n return this.resolvedLinksCache.get(result.ID)!;\n }\n\n const links: { Value: string; Label: string; Type: 'URL' | 'Email' }[] = [];\n\n if (result.RawMetadata) {\n try {\n const meta = JSON.parse(result.RawMetadata) as Record<string, unknown>;\n const linkFields = this.GetLinkFields(result.EntityName);\n\n for (const field of linkFields) {\n const value = meta[field.Name];\n if (value && typeof value === 'string' && value.trim().length > 0) {\n links.push({\n Value: value,\n Label: field.DisplayName,\n Type: field.Type\n });\n }\n }\n } catch { /* ignore parse errors */ }\n }\n\n // Fallback: extract URLs from snippet if no ExtendedType fields found\n if (links.length === 0) {\n const urls = this.ExtractUrls(result.Snippet);\n for (const url of urls) {\n links.push({ Value: url, Label: '', Type: 'URL' });\n }\n }\n\n this.resolvedLinksCache.set(result.ID, links);\n return links;\n }\n\n /** Extract URLs from snippet text */\n public ExtractUrls(snippet: string): string[] {\n if (!snippet) return [];\n const urlRegex = /https?:\\/\\/[^\\s,;)>\"']+/g;\n const matches = snippet.match(urlRegex);\n return matches ? [...new Set(matches)] : [];\n }\n\n /** Truncate a URL for display */\n public TruncateUrl(url: string): string {\n try {\n const parsed = new URL(url);\n const path = parsed.pathname.length > 30\n ? parsed.pathname.substring(0, 30) + '...'\n : parsed.pathname;\n return parsed.hostname + path;\n } catch {\n return url.length > 50 ? url.substring(0, 50) + '...' : url;\n }\n }\n\n /** Cache of URL/Email field info per entity name */\n private linkFieldsCache = new Map<string, { Name: string; DisplayName: string; Type: 'URL' | 'Email' }[]>();\n\n /**\n * Get URL and Email fields for an entity using ExtendedType from entity metadata.\n * Returns fields with ExtendedType='URL' or ExtendedType='Email'.\n */\n public GetLinkFields(entityName: string): { Name: string; DisplayName: string; Type: 'URL' | 'Email' }[] {\n if (this.linkFieldsCache.has(entityName)) {\n return this.linkFieldsCache.get(entityName)!;\n }\n\n try {\n const md = new Metadata();\n const entityInfo = md.Entities.find(e => e.Name === entityName);\n if (!entityInfo) {\n this.linkFieldsCache.set(entityName, []);\n return [];\n }\n\n const linkFields = entityInfo.Fields\n .filter(f => f.ExtendedType === 'URL' || f.ExtendedType === 'Email')\n .map(f => ({\n Name: f.Name,\n DisplayName: f.DisplayNameOrName,\n Type: f.ExtendedType as 'URL' | 'Email'\n }));\n\n this.linkFieldsCache.set(entityName, linkFields);\n return linkFields;\n } catch {\n this.linkFieldsCache.set(entityName, []);\n return [];\n }\n }\n}\n","@if (IsLoading) {\n <div class=\"results-loading\">\n <mj-loading text=\"Searching...\" size=\"medium\"></mj-loading>\n </div>\n} @else if ((DisplayMode === 'flat' ? FlatResults.length : ResultGroups.length) === 0 && TotalCount === 0) {\n <div class=\"results-empty\">\n <i class=\"fa-solid fa-search results-empty-icon\"></i>\n <p class=\"results-empty-text\">No results to display</p>\n </div>\n} @else {\n <!-- Results summary -->\n @if (ShowSummary) {\n <div class=\"results-summary\">\n <span>{{ TotalCount }} results</span>\n @if (ElapsedMs > 0) {\n <span class=\"results-summary-time\"> in {{ FormatTime(ElapsedMs) }}</span>\n }\n </div>\n }\n\n @if (DisplayMode === 'flat') {\n <!-- ══════ FLAT BLENDED LIST (sorted by score) ══════ -->\n <div class=\"results-scroll-area\">\n @for (result of PagedResults; track result.ID) {\n <ng-container *ngTemplateOutlet=\"resultCardTpl; context: { $implicit: result }\"></ng-container>\n }\n </div>\n\n <!-- Google-style pagination (sticky at bottom) -->\n @if (TotalPages > 1) {\n <div class=\"results-pager\">\n <button class=\"pager-btn pager-prev\" [disabled]=\"CurrentPage === 1\"\n (click)=\"GoToPage(CurrentPage - 1)\">\n <i class=\"fa-solid fa-chevron-left\"></i>\n </button>\n @for (p of PageNumbers; track $index) {\n @if (p === -1) {\n <span class=\"pager-ellipsis\">...</span>\n } @else {\n <button class=\"pager-btn pager-num\" [class.pager-active]=\"p === CurrentPage\"\n (click)=\"GoToPage(p)\">{{ p }}</button>\n }\n }\n <button class=\"pager-btn pager-next\" [disabled]=\"CurrentPage === TotalPages\"\n (click)=\"GoToPage(CurrentPage + 1)\">\n <i class=\"fa-solid fa-chevron-right\"></i>\n </button>\n </div>\n }\n } @else {\n <!-- ══════ GROUPED BY SOURCE TYPE ══════ -->\n @for (group of ResultGroups; track group.SourceType) {\n <div class=\"results-group\">\n <div class=\"results-group-header\" (click)=\"ToggleGroup(group.SourceType)\">\n @if (ShowSourceIcons) {\n <i [class]=\"group.Icon + ' results-group-icon'\"></i>\n }\n <span class=\"results-group-label\">{{ group.Label }}</span>\n <span class=\"results-group-count\">({{ group.TotalCount }})</span>\n <i class=\"fa-solid results-group-chevron\"\n [class.fa-chevron-down]=\"!IsGroupExpanded(group.SourceType)\"\n [class.fa-chevron-up]=\"IsGroupExpanded(group.SourceType)\"></i>\n </div>\n\n @for (result of GetVisibleResults(group); track result.ID) {\n <ng-container *ngTemplateOutlet=\"resultCardTpl; context: { $implicit: result }\"></ng-container>\n }\n\n @if (HasMoreResults(group)) {\n <button class=\"results-show-more\" (click)=\"ToggleGroup(group.SourceType)\">\n <i class=\"fa-solid fa-chevron-down\"></i>\n Show {{ GetHiddenCount(group) }} more results\n </button>\n }\n </div>\n }\n }\n}\n\n<!-- ═══ Shared result card template ═══ -->\n<ng-template #resultCardTpl let-result>\n <div class=\"result-card\" [class.result-card-expanded]=\"IsExpanded(result.ID)\">\n <!-- Main row -->\n <div class=\"result-card-main\" (click)=\"ToggleExpand(result)\">\n <div class=\"result-card-icon-wrap\">\n <i [class]=\"result.EntityIcon || result.SourceIcon || 'fa-solid fa-database'\"></i>\n </div>\n <div class=\"result-card-body\">\n <div class=\"result-card-title\" [innerHTML]=\"HighlightMatch(result.RecordName || result.Title)\"></div>\n <div class=\"result-card-entity\" [innerHTML]=\"HighlightMatch(result.EntityName)\"></div>\n @if (result.Tags && result.Tags.length > 0) {\n <div class=\"result-card-tags\">\n @for (tag of result.Tags.slice(0, 5); track tag) {\n <span class=\"result-tag-chip\" [innerHTML]=\"HighlightMatch(tag)\"></span>\n }\n @if (result.Tags.length > 5) {\n <span class=\"result-tag-more\">+{{ result.Tags.length - 5 }}</span>\n }\n </div>\n }\n </div>\n @if (ShowScores) {\n <div class=\"result-card-score\">\n <div class=\"score-circle\" [class.score-high]=\"result.Score >= 0.40\" [class.score-medium]=\"result.Score >= 0.15 && result.Score < 0.40\" [class.score-low]=\"result.Score < 0.15\">\n <span class=\"score-value\">{{ FormatScore(result.Score) }}</span>\n </div>\n </div>\n }\n <div class=\"result-card-actions\">\n <button class=\"result-open-btn\" (click)=\"OnOpenRecord(result, $event)\"\n [title]=\"result.ResultType === 'storage-file' ? 'Open file' : 'Open record'\">\n <i [class]=\"result.ResultType === 'storage-file' ? 'fa-solid fa-external-link' : 'fa-solid fa-arrow-up-right-from-square'\"></i>\n </button>\n <button class=\"result-expand-btn\" (click)=\"ToggleExpand(result, $event)\" title=\"Expand details\">\n <i class=\"fa-solid\" [class.fa-chevron-down]=\"!IsExpanded(result.ID)\"\n [class.fa-chevron-up]=\"IsExpanded(result.ID)\"></i>\n </button>\n </div>\n </div>\n\n <!-- Expanded detail -->\n @if (IsExpanded(result.ID)) {\n <div class=\"result-card-detail\">\n @if (result.Snippet) {\n <div class=\"detail-section\">\n <span class=\"detail-label\">Preview</span>\n <p class=\"detail-snippet\" [innerHTML]=\"HighlightMatch(result.Snippet)\"></p>\n </div>\n }\n <div class=\"detail-meta\">\n <div class=\"detail-meta-item\">\n <i class=\"fa-solid fa-layer-group\"></i>\n <span>{{ result.EntityName }}</span>\n </div>\n <div class=\"detail-meta-item\">\n <i class=\"fa-solid fa-fingerprint\"></i>\n <span class=\"detail-record-id\">{{ result.RecordID }}</span>\n </div>\n <!-- SR-1: Show overall score consistently -->\n <div class=\"detail-meta-item\">\n <i class=\"fa-solid fa-signal\"></i>\n <span>Relevance: {{ FormatScore(result.Score) }}</span>\n </div>\n @if (result.ScoreBreakdown) {\n <div class=\"detail-meta-item\">\n <i class=\"fa-solid fa-chart-bar\"></i>\n <span>\n @if (result.ScoreBreakdown.Vector) { Vector: {{ FormatScore(result.ScoreBreakdown.Vector) }} }\n @if (result.ScoreBreakdown.FullText) { FTS: {{ FormatScore(result.ScoreBreakdown.FullText) }} }\n </span>\n </div>\n }\n </div>\n @if (ShowTags && result.Tags.length > 0) {\n <div class=\"detail-tags\">\n @for (tag of result.Tags; track tag) {\n <span class=\"result-card-tag\" [innerHTML]=\"HighlightMatch(tag)\"></span>\n }\n </div>\n }\n @if (GetResolvedLinks(result).length > 0) {\n <div class=\"detail-links\">\n @for (link of GetResolvedLinks(result); track link.Value) {\n @if (link.Type === 'URL') {\n <a [href]=\"link.Value\" target=\"_blank\" class=\"detail-link\" (click)=\"$event.stopPropagation()\">\n <i class=\"fa-solid fa-arrow-up-right-from-square\"></i>\n {{ TruncateUrl(link.Value) }}\n </a>\n } @else if (link.Type === 'Email') {\n <a [href]=\"'mailto:' + link.Value\" class=\"detail-link detail-link-email\" (click)=\"$event.stopPropagation()\">\n <i class=\"fa-solid fa-envelope\"></i>\n {{ link.Value }}\n </a>\n }\n }\n </div>\n }\n <div class=\"detail-actions\">\n <button class=\"detail-open-btn\" (click)=\"OnOpenRecord(result, $event)\">\n @if (result.ResultType === 'storage-file') {\n <i class=\"fa-solid fa-external-link\"></i> Open File\n } @else {\n <i class=\"fa-solid fa-arrow-up-right-from-square\"></i> Open Record\n }\n </button>\n <button class=\"detail-similar-btn\" (click)=\"OnMoreLikeThis(result, $event)\">\n <i class=\"fa-solid fa-shuffle\"></i> See Similar Items\n </button>\n </div>\n </div>\n }\n </div>\n</ng-template>\n"]}
1
+ {"version":3,"file":"search-results.component.js","sourceRoot":"","sources":["../../src/lib/search-results.component.ts","../../src/lib/search-results.component.html"],"names":[],"mappings":"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","sourcesContent":["/**\n * @fileoverview Search Results Component\n *\n * Displays search results grouped by source type with expandable sections.\n * Used both within the search overlay and as a standalone results view\n * in the Knowledge Hub search dashboard.\n */\n\nimport {\n Component,\n EventEmitter,\n Input,\n Output,\n ChangeDetectorRef,\n inject\n} from '@angular/core';\nimport { SearchResultItem, SearchResultGroup, SearchResultSelectedEvent } from './search-types';\nimport { Metadata } from '@memberjunction/core';\nimport { EscapeHTML } from '@memberjunction/global';\n\n@Component({\n standalone: false,\n selector: 'mj-search-results',\n templateUrl: './search-results.component.html',\n styleUrls: ['./search-results.component.css']\n})\nexport class SearchResultsComponent {\n private cdr = inject(ChangeDetectorRef);\n\n /** Grouped search results to display (used when DisplayMode is 'grouped') */\n @Input() ResultGroups: SearchResultGroup[] = [];\n\n /** Flat list of all results sorted by score (used when DisplayMode is 'flat') */\n @Input() FlatResults: SearchResultItem[] = [];\n\n /** Display mode: 'flat' for blended list sorted by score, 'grouped' for source-type groups */\n @Input() DisplayMode: 'flat' | 'grouped' = 'flat';\n\n /** Whether results are currently loading */\n @Input() IsLoading = false;\n\n /** Total result count */\n @Input() TotalCount = 0;\n\n /** Elapsed search time in ms */\n @Input() ElapsedMs = 0;\n\n /** Maximum results per group before \"Show more\" (grouped mode only) */\n @Input() MaxPerGroup = 5;\n\n /** Results per page in flat mode */\n @Input() PageSize = 10;\n\n /** Whether to show the results summary line (count + time). Set false when parent provides its own header. */\n @Input() ShowSummary = true;\n\n /** Whether to show score badges */\n @Input() ShowScores = true;\n\n /** Whether to show tags */\n @Input() ShowTags = true;\n\n /** Whether to show source type icons */\n @Input() ShowSourceIcons = true;\n\n /** Text to highlight within result cards (e.g. from a client-side filter) */\n @Input() HighlightText = '';\n\n @Output() ResultSelected = new EventEmitter<SearchResultSelectedEvent>();\n\n /** Emitted when user clicks \"Open Record\" — parent handles navigation */\n @Output() OpenRecordRequested = new EventEmitter<SearchResultItem>();\n\n /** Emitted when user clicks \"See Similar Items\" — parent runs a \"more like this\" search */\n @Output() MoreLikeThisRequested = new EventEmitter<SearchResultItem>();\n\n /** Current page (1-based) for flat mode pagination */\n public CurrentPage = 1;\n\n /** Tracks which groups are expanded */\n public ExpandedGroups = new Set<string>();\n\n /** Set of expanded result card IDs — multiple can be open simultaneously */\n public ExpandedResultIDs = new Set<string>();\n\n // ── Flat mode pagination ──\n\n /** Get results for the current page in flat mode */\n public get PagedResults(): SearchResultItem[] {\n const start = (this.CurrentPage - 1) * this.PageSize;\n return this.FlatResults.slice(start, start + this.PageSize);\n }\n\n /** Total number of pages */\n public get TotalPages(): number {\n return Math.ceil(this.FlatResults.length / this.PageSize);\n }\n\n /** Page numbers to display in the pager */\n public get PageNumbers(): number[] {\n const total = this.TotalPages;\n if (total <= 7) return Array.from({ length: total }, (_, i) => i + 1);\n\n const pages: number[] = [1];\n const start = Math.max(2, this.CurrentPage - 2);\n const end = Math.min(total - 1, this.CurrentPage + 2);\n if (start > 2) pages.push(-1); // ellipsis marker\n for (let i = start; i <= end; i++) pages.push(i);\n if (end < total - 1) pages.push(-1); // ellipsis marker\n pages.push(total);\n return pages;\n }\n\n /** Navigate to a specific page */\n public GoToPage(page: number): void {\n if (page < 1 || page > this.TotalPages) return;\n this.CurrentPage = page;\n this.cdr.detectChanges();\n }\n\n // ── Grouped mode ──\n\n /** Toggle group expansion */\n public ToggleGroup(sourceType: string): void {\n if (this.ExpandedGroups.has(sourceType)) {\n this.ExpandedGroups.delete(sourceType);\n } else {\n this.ExpandedGroups.add(sourceType);\n }\n this.cdr.detectChanges();\n }\n\n /** Whether a group is expanded */\n public IsGroupExpanded(sourceType: string): boolean {\n return this.ExpandedGroups.has(sourceType);\n }\n\n /** Get visible results for a group (respecting max per group unless expanded) */\n public GetVisibleResults(group: SearchResultGroup): SearchResultItem[] {\n if (this.IsGroupExpanded(group.SourceType)) {\n return group.Results;\n }\n return group.Results.slice(0, this.MaxPerGroup);\n }\n\n /** Whether a group has more results than shown */\n public HasMoreResults(group: SearchResultGroup): boolean {\n return group.Results.length > this.MaxPerGroup && !this.IsGroupExpanded(group.SourceType);\n }\n\n /** Get the count of hidden results */\n public GetHiddenCount(group: SearchResultGroup): number {\n return group.Results.length - this.MaxPerGroup;\n }\n\n /** Handle clicking a result */\n public OnResultClick(result: SearchResultItem, event: MouseEvent): void {\n this.ResultSelected.emit({\n Result: result,\n OpenInNewTab: event.metaKey || event.ctrlKey\n });\n }\n\n /** Toggle expand/collapse of a result card — multiple can be open */\n public ToggleExpand(result: SearchResultItem, event?: MouseEvent): void {\n if (event) event.stopPropagation();\n if (this.ExpandedResultIDs.has(result.ID)) {\n this.ExpandedResultIDs.delete(result.ID);\n } else {\n this.ExpandedResultIDs.add(result.ID);\n }\n this.cdr.detectChanges();\n }\n\n /** Check if a result card is expanded */\n public IsExpanded(resultID: string): boolean {\n return this.ExpandedResultIDs.has(resultID);\n }\n\n /** Handle \"Open Record\" button click */\n public OnOpenRecord(result: SearchResultItem, event: MouseEvent): void {\n event.stopPropagation();\n this.OpenRecordRequested.emit(result);\n }\n\n /** Handle \"See Similar Items\" button click — emits the result for a \"more like this\" search */\n public OnMoreLikeThis(result: SearchResultItem, event: MouseEvent): void {\n event.stopPropagation();\n this.MoreLikeThisRequested.emit(result);\n }\n\n /**\n * Wrap substrings matching HighlightText in <mark> tags for visual emphasis.\n * The input is regex-escaped so user text is treated as a literal string.\n */\n public HighlightMatch(text: string): string {\n if (!text) return text;\n const safeText = EscapeHTML(text);\n if (!this.HighlightText) return safeText;\n\n const safeHighlightText = EscapeHTML(this.HighlightText);\n const escaped = safeHighlightText.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n const regex = new RegExp(`(${escaped})`, 'gi');\n return safeText.replace(regex, '<mark class=\"search-highlight\">$1</mark>');\n }\n\n /** Format a score as a percentage */\n public FormatScore(score: number): string {\n return `${Math.round(score * 100)}%`;\n }\n\n /** Format elapsed time */\n public FormatTime(ms: number): string {\n if (ms < 1000) return `${ms}ms`;\n return `${(ms / 1000).toFixed(1)}s`;\n }\n\n /** Resolved link cache per result ID */\n private resolvedLinksCache = new Map<string, { Value: string; Label: string; Type: 'URL' | 'Email' }[]>();\n\n /**\n * Resolve URL and Email fields from the result's RawMetadata using entity ExtendedType metadata.\n * Returns clickable link entries with proper type classification.\n */\n public GetResolvedLinks(result: SearchResultItem): { Value: string; Label: string; Type: 'URL' | 'Email' }[] {\n if (this.resolvedLinksCache.has(result.ID)) {\n return this.resolvedLinksCache.get(result.ID)!;\n }\n\n const links: { Value: string; Label: string; Type: 'URL' | 'Email' }[] = [];\n\n if (result.RawMetadata) {\n try {\n const meta = JSON.parse(result.RawMetadata) as Record<string, unknown>;\n const linkFields = this.GetLinkFields(result.EntityName);\n\n for (const field of linkFields) {\n const value = meta[field.Name];\n if (value && typeof value === 'string' && value.trim().length > 0) {\n links.push({\n Value: value,\n Label: field.DisplayName,\n Type: field.Type\n });\n }\n }\n } catch { /* ignore parse errors */ }\n }\n\n // Fallback: extract URLs from snippet if no ExtendedType fields found\n if (links.length === 0) {\n const urls = this.ExtractUrls(result.Snippet);\n for (const url of urls) {\n links.push({ Value: url, Label: '', Type: 'URL' });\n }\n }\n\n this.resolvedLinksCache.set(result.ID, links);\n return links;\n }\n\n /** Extract URLs from snippet text */\n public ExtractUrls(snippet: string): string[] {\n if (!snippet) return [];\n const urlRegex = /https?:\\/\\/[^\\s,;)>\"']+/g;\n const matches = snippet.match(urlRegex);\n return matches ? [...new Set(matches)] : [];\n }\n\n /** Truncate a URL for display */\n public TruncateUrl(url: string): string {\n try {\n const parsed = new URL(url);\n const path = parsed.pathname.length > 30\n ? parsed.pathname.substring(0, 30) + '...'\n : parsed.pathname;\n return parsed.hostname + path;\n } catch {\n return url.length > 50 ? url.substring(0, 50) + '...' : url;\n }\n }\n\n /** Cache of URL/Email field info per entity name */\n private linkFieldsCache = new Map<string, { Name: string; DisplayName: string; Type: 'URL' | 'Email' }[]>();\n\n /**\n * Get URL and Email fields for an entity using ExtendedType from entity metadata.\n * Returns fields with ExtendedType='URL' or ExtendedType='Email'.\n */\n public GetLinkFields(entityName: string): { Name: string; DisplayName: string; Type: 'URL' | 'Email' }[] {\n if (this.linkFieldsCache.has(entityName)) {\n return this.linkFieldsCache.get(entityName)!;\n }\n\n try {\n const md = new Metadata();\n const entityInfo = md.Entities.find(e => e.Name === entityName);\n if (!entityInfo) {\n this.linkFieldsCache.set(entityName, []);\n return [];\n }\n\n const linkFields = entityInfo.Fields\n .filter(f => f.ExtendedType === 'URL' || f.ExtendedType === 'Email')\n .map(f => ({\n Name: f.Name,\n DisplayName: f.DisplayNameOrName,\n Type: f.ExtendedType as 'URL' | 'Email'\n }));\n\n this.linkFieldsCache.set(entityName, linkFields);\n return linkFields;\n } catch {\n this.linkFieldsCache.set(entityName, []);\n return [];\n }\n }\n}\n","@if (IsLoading) {\n <div class=\"results-loading\">\n <mj-loading text=\"Searching...\" size=\"medium\"></mj-loading>\n </div>\n} @else if ((DisplayMode === 'flat' ? FlatResults.length : ResultGroups.length) === 0 && TotalCount === 0) {\n <div class=\"results-empty\">\n <i class=\"fa-solid fa-search results-empty-icon\"></i>\n <p class=\"results-empty-text\">No results to display</p>\n </div>\n} @else {\n <!-- Results summary -->\n @if (ShowSummary) {\n <div class=\"results-summary\">\n <span>{{ TotalCount }} results</span>\n @if (ElapsedMs > 0) {\n <span class=\"results-summary-time\"> in {{ FormatTime(ElapsedMs) }}</span>\n }\n </div>\n }\n\n @if (DisplayMode === 'flat') {\n <!-- ══════ FLAT BLENDED LIST (sorted by score) ══════ -->\n <div class=\"results-scroll-area\">\n @for (result of PagedResults; track result.ID) {\n <ng-container *ngTemplateOutlet=\"resultCardTpl; context: { $implicit: result }\"></ng-container>\n }\n </div>\n\n <!-- Google-style pagination (sticky at bottom) -->\n @if (TotalPages > 1) {\n <div class=\"results-pager\">\n <button class=\"pager-btn pager-prev\" [disabled]=\"CurrentPage === 1\"\n (click)=\"GoToPage(CurrentPage - 1)\" aria-label=\"Previous page\">\n <i class=\"fa-solid fa-chevron-left\"></i>\n </button>\n @for (p of PageNumbers; track $index) {\n @if (p === -1) {\n <span class=\"pager-ellipsis\">...</span>\n } @else {\n <button class=\"pager-btn pager-num\" [class.pager-active]=\"p === CurrentPage\"\n (click)=\"GoToPage(p)\">{{ p }}</button>\n }\n }\n <button class=\"pager-btn pager-next\" [disabled]=\"CurrentPage === TotalPages\"\n (click)=\"GoToPage(CurrentPage + 1)\" aria-label=\"Next page\">\n <i class=\"fa-solid fa-chevron-right\"></i>\n </button>\n </div>\n }\n } @else {\n <!-- ══════ GROUPED BY SOURCE TYPE ══════ -->\n @for (group of ResultGroups; track group.SourceType) {\n <div class=\"results-group\">\n <div class=\"results-group-header\" (click)=\"ToggleGroup(group.SourceType)\">\n @if (ShowSourceIcons) {\n <i [class]=\"group.Icon + ' results-group-icon'\"></i>\n }\n <span class=\"results-group-label\">{{ group.Label }}</span>\n <span class=\"results-group-count\">({{ group.TotalCount }})</span>\n <i class=\"fa-solid results-group-chevron\"\n [class.fa-chevron-down]=\"!IsGroupExpanded(group.SourceType)\"\n [class.fa-chevron-up]=\"IsGroupExpanded(group.SourceType)\"></i>\n </div>\n\n @for (result of GetVisibleResults(group); track result.ID) {\n <ng-container *ngTemplateOutlet=\"resultCardTpl; context: { $implicit: result }\"></ng-container>\n }\n\n @if (HasMoreResults(group)) {\n <button class=\"results-show-more\" (click)=\"ToggleGroup(group.SourceType)\">\n <i class=\"fa-solid fa-chevron-down\"></i>\n Show {{ GetHiddenCount(group) }} more results\n </button>\n }\n </div>\n }\n }\n}\n\n<!-- ═══ Shared result card template ═══ -->\n<ng-template #resultCardTpl let-result>\n <div class=\"result-card\" [class.result-card-expanded]=\"IsExpanded(result.ID)\">\n <!-- Main row -->\n <div class=\"result-card-main\" (click)=\"ToggleExpand(result)\">\n <div class=\"result-card-icon-wrap\">\n <i [class]=\"result.EntityIcon || result.SourceIcon || 'fa-solid fa-database'\"></i>\n </div>\n <div class=\"result-card-body\">\n <div class=\"result-card-title\" [innerHTML]=\"HighlightMatch(result.RecordName || result.Title)\"></div>\n <div class=\"result-card-entity\" [innerHTML]=\"HighlightMatch(result.EntityName)\"></div>\n @if (result.Tags && result.Tags.length > 0) {\n <div class=\"result-card-tags\">\n @for (tag of result.Tags.slice(0, 5); track tag) {\n <span class=\"result-tag-chip\" [innerHTML]=\"HighlightMatch(tag)\"></span>\n }\n @if (result.Tags.length > 5) {\n <span class=\"result-tag-more\">+{{ result.Tags.length - 5 }}</span>\n }\n </div>\n }\n </div>\n @if (ShowScores) {\n <div class=\"result-card-score\">\n <div class=\"score-circle\" [class.score-high]=\"result.Score >= 0.40\" [class.score-medium]=\"result.Score >= 0.15 && result.Score < 0.40\" [class.score-low]=\"result.Score < 0.15\">\n <span class=\"score-value\">{{ FormatScore(result.Score) }}</span>\n </div>\n </div>\n }\n <div class=\"result-card-actions\">\n <button class=\"result-open-btn\" (click)=\"OnOpenRecord(result, $event)\"\n [title]=\"result.ResultType === 'storage-file' ? 'Open file' : 'Open record'\"\n [attr.aria-label]=\"result.ResultType === 'storage-file' ? 'Open file' : 'Open record'\">\n <i [class]=\"result.ResultType === 'storage-file' ? 'fa-solid fa-external-link' : 'fa-solid fa-arrow-up-right-from-square'\"></i>\n </button>\n <button class=\"result-expand-btn\" (click)=\"ToggleExpand(result, $event)\" title=\"Expand details\" aria-label=\"Expand details\">\n <i class=\"fa-solid\" [class.fa-chevron-down]=\"!IsExpanded(result.ID)\"\n [class.fa-chevron-up]=\"IsExpanded(result.ID)\"></i>\n </button>\n </div>\n </div>\n\n <!-- Expanded detail -->\n @if (IsExpanded(result.ID)) {\n <div class=\"result-card-detail\">\n @if (result.Snippet) {\n <div class=\"detail-section\">\n <span class=\"detail-label\">Preview</span>\n <p class=\"detail-snippet\" [innerHTML]=\"HighlightMatch(result.Snippet)\"></p>\n </div>\n }\n <div class=\"detail-meta\">\n <div class=\"detail-meta-item\">\n <i class=\"fa-solid fa-layer-group\"></i>\n <span>{{ result.EntityName }}</span>\n </div>\n <div class=\"detail-meta-item\">\n <i class=\"fa-solid fa-fingerprint\"></i>\n <span class=\"detail-record-id\">{{ result.RecordID }}</span>\n </div>\n <!-- SR-1: Show overall score consistently -->\n <div class=\"detail-meta-item\">\n <i class=\"fa-solid fa-signal\"></i>\n <span>Relevance: {{ FormatScore(result.Score) }}</span>\n </div>\n @if (result.ScoreBreakdown) {\n <div class=\"detail-meta-item\">\n <i class=\"fa-solid fa-chart-bar\"></i>\n <span>\n @if (result.ScoreBreakdown.Vector) { Vector: {{ FormatScore(result.ScoreBreakdown.Vector) }} }\n @if (result.ScoreBreakdown.FullText) { FTS: {{ FormatScore(result.ScoreBreakdown.FullText) }} }\n </span>\n </div>\n }\n </div>\n @if (ShowTags && result.Tags.length > 0) {\n <div class=\"detail-tags\">\n @for (tag of result.Tags; track tag) {\n <span class=\"result-card-tag\" [innerHTML]=\"HighlightMatch(tag)\"></span>\n }\n </div>\n }\n @if (GetResolvedLinks(result).length > 0) {\n <div class=\"detail-links\">\n @for (link of GetResolvedLinks(result); track link.Value) {\n @if (link.Type === 'URL') {\n <a [href]=\"link.Value\" target=\"_blank\" class=\"detail-link\" (click)=\"$event.stopPropagation()\">\n <i class=\"fa-solid fa-arrow-up-right-from-square\"></i>\n {{ TruncateUrl(link.Value) }}\n </a>\n } @else if (link.Type === 'Email') {\n <a [href]=\"'mailto:' + link.Value\" class=\"detail-link detail-link-email\" (click)=\"$event.stopPropagation()\">\n <i class=\"fa-solid fa-envelope\"></i>\n {{ link.Value }}\n </a>\n }\n }\n </div>\n }\n <div class=\"detail-actions\">\n <button class=\"detail-open-btn\" (click)=\"OnOpenRecord(result, $event)\">\n @if (result.ResultType === 'storage-file') {\n <i class=\"fa-solid fa-external-link\"></i> Open File\n } @else {\n <i class=\"fa-solid fa-arrow-up-right-from-square\"></i> Open Record\n }\n </button>\n <button class=\"detail-similar-btn\" (click)=\"OnMoreLikeThis(result, $event)\">\n <i class=\"fa-solid fa-shuffle\"></i> See Similar Items\n </button>\n </div>\n </div>\n }\n </div>\n</ng-template>\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@memberjunction/ng-search",
3
- "version": "5.29.0",
3
+ "version": "5.30.0",
4
4
  "description": "MemberJunction: Reusable search overlay and results components - command palette / Spotlight style search",
5
5
  "main": "./dist/public-api.js",
6
6
  "typings": "./dist/public-api.d.ts",
@@ -27,11 +27,11 @@
27
27
  "@angular/forms": "21.1.3"
28
28
  },
29
29
  "dependencies": {
30
- "@memberjunction/core": "5.29.0",
31
- "@memberjunction/global": "5.29.0",
32
- "@memberjunction/graphql-dataprovider": "5.29.0",
33
- "@memberjunction/core-entities": "5.29.0",
34
- "@memberjunction/ng-shared-generic": "5.29.0",
30
+ "@memberjunction/core": "5.30.0",
31
+ "@memberjunction/global": "5.30.0",
32
+ "@memberjunction/graphql-dataprovider": "5.30.0",
33
+ "@memberjunction/core-entities": "5.30.0",
34
+ "@memberjunction/ng-shared-generic": "5.30.0",
35
35
  "rxjs": "^7.8.2",
36
36
  "tslib": "^2.8.1"
37
37
  },