@maro-tech/filters 0.1.0 → 0.1.2

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.
package/README.md CHANGED
@@ -16,4 +16,4 @@ const config: FilterPanelConfig = {
16
16
  <maro-filter-panel [config]="config" [value]="filters" (valueChange)="filters = $event" />
17
17
  ```
18
18
 
19
- Quick fields apply immediately. Advanced fields stay in a draft until the user selects **Apply**. Search is always rendered first and applies after a fixed 300 ms debounce. The component emits values only; API requests and saved-filter persistence remain the consuming application's responsibility.
19
+ Quick and advanced fields apply immediately; the advanced panel stays open while values change. Search is always rendered first and applies after a fixed 300 ms debounce. The component emits values only; API requests and saved-filter persistence remain the consuming application's responsibility.
@@ -1,16 +1,18 @@
1
1
  import { CommonModule } from '@angular/common';
2
2
  import * as i0 from '@angular/core';
3
- import { EventEmitter, Output, Input, Component } from '@angular/core';
3
+ import { EventEmitter, Output, Input, ViewChildren, Component } from '@angular/core';
4
4
  import * as i1 from '@angular/forms';
5
5
  import { FormsModule } from '@angular/forms';
6
- import { LucideDynamicIcon } from '@lucide/angular';
6
+ import { LucideSlidersHorizontal, LucideX, LucideDynamicIcon } from '@lucide/angular';
7
7
  import { InputComponent, InputSelectComponent } from '@maro-tech/form';
8
8
 
9
9
  class FilterPanelComponent {
10
+ selectInputs;
10
11
  config;
11
12
  value = {};
12
- resultCount;
13
13
  valueChange = new EventEmitter();
14
+ filtersIcon = LucideSlidersHorizontal;
15
+ closeIcon = LucideX;
14
16
  draft = {};
15
17
  advancedOpen = false;
16
18
  searchTimer;
@@ -20,6 +22,12 @@ class FilterPanelComponent {
20
22
  get advancedFields() {
21
23
  return (this.config?.fields ?? []).filter((field) => field.placement !== 'quick');
22
24
  }
25
+ get activeFilterCount() {
26
+ return (this.config?.fields ?? []).filter((field) => {
27
+ const value = this.value?.[field.id];
28
+ return value !== '' && value !== null && value !== undefined && (!Array.isArray(value) || value.length > 0);
29
+ }).length;
30
+ }
23
31
  get activeEntries() {
24
32
  const fields = this.config?.fields ?? [];
25
33
  return Object.entries(this.value ?? {}).flatMap(([id, raw]) => {
@@ -31,7 +39,10 @@ class FilterPanelComponent {
31
39
  if (!field)
32
40
  return [];
33
41
  const values = Array.isArray(raw) ? raw : [raw];
34
- const labels = values.map((entry) => field.options?.find((option) => String(option.value) === String(entry))?.label ?? String(entry));
42
+ const selectedLabels = this.selectInputs?.find((input) => input.name === id)?.selectedLabelsByValue;
43
+ const labels = values.map((entry) => field.options?.find((option) => String(option.value) === String(entry))?.label
44
+ ?? selectedLabels?.[String(entry)]
45
+ ?? String(entry));
35
46
  return [{ id, label: field.label, text: `${field.label}: ${labels.join(', ')}` }];
36
47
  });
37
48
  }
@@ -41,11 +52,7 @@ class FilterPanelComponent {
41
52
  }
42
53
  setField(id, value) {
43
54
  this.draft = { ...this.draft, [id]: value };
44
- const field = this.config.fields.find((item) => item.id === id);
45
- if (field?.placement === 'quick') {
46
- const next = { ...this.value, [id]: value };
47
- this.valueChange.emit(next);
48
- }
55
+ this.valueChange.emit({ ...this.value, [id]: value });
49
56
  }
50
57
  setSearch(value) {
51
58
  const id = this.config.search?.id;
@@ -59,14 +66,9 @@ class FilterPanelComponent {
59
66
  }, 300);
60
67
  }
61
68
  apply() {
62
- if (this.searchTimer)
63
- clearTimeout(this.searchTimer);
64
- this.searchTimer = undefined;
65
- this.valueChange.emit({ ...this.draft });
66
69
  this.advancedOpen = false;
67
70
  }
68
71
  cancel() {
69
- this.draft = { ...(this.value ?? {}) };
70
72
  this.advancedOpen = false;
71
73
  }
72
74
  toggleAdvanced() {
@@ -99,18 +101,19 @@ class FilterPanelComponent {
99
101
  clearTimeout(this.searchTimer);
100
102
  }
101
103
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: FilterPanelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
102
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: FilterPanelComponent, isStandalone: true, selector: "maro-filter-panel", inputs: { config: "config", value: "value", resultCount: "resultCount" }, outputs: { valueChange: "valueChange" }, usesOnChanges: true, ngImport: i0, template: "<section class=\"rounded-2xl border border-slate-200 bg-white p-4 shadow-sm\">\n <div class=\"flex flex-wrap items-end gap-3\">\n @if (config.search; as search) {\n <div class=\"min-w-[15rem] flex-[1.2]\">\n <input-base [icon]=\"search.icon || 'search'\" [placeholder]=\"search.placeholder || '\u041F\u043E\u0448\u0443\u043A\u2026'\" [ngModel]=\"draft[search.id] || ''\" (ngModelChange)=\"setSearch($event)\" />\n </div>\n }\n @for (field of quickFields; track field.id) {\n <div class=\"min-w-[10rem] flex-1\">\n @if (field.type === 'select' || field.type === 'boolean') {\n <input-select [label]=\"field.label\" [icon]=\"field.icon\" [data]=\"field.data || ''\" [options]=\"options(field)\" [multiple]=\"!!field.multiple\" [searchable]=\"!!field.data\" [placeholder]=\"field.placeholder || '\u041E\u0431\u0435\u0440\u0456\u0442\u044C\u2026'\" [ngModel]=\"fieldValue(field)\" (ngModelChange)=\"setField(field.id, $event)\" />\n } @else {\n <input-base [label]=\"field.label\" [icon]=\"field.icon\" [type]=\"field.type === 'number' ? 'number' : field.type === 'date' ? 'date' : 'text'\" [placeholder]=\"field.placeholder || ''\" [ngModel]=\"fieldValue(field)\" (ngModelChange)=\"setField(field.id, $event)\" />\n }\n </div>\n }\n @if (advancedFields.length) {\n <button type=\"button\" class=\"flex h-10 items-center gap-2 rounded-xl border border-blue-300 px-4 text-sm font-semibold text-blue-600 hover:bg-blue-50\" (click)=\"advancedOpen = !advancedOpen\">\n <svg [lucideIcon]=\"'sliders-horizontal'\" class=\"h-4 w-4\"></svg>{{ config.labels?.filters || '\u0424\u0456\u043B\u044C\u0442\u0440\u0438' }}\n @if (activeEntries.length) { <span class=\"rounded-full bg-blue-100 px-1.5 text-xs\">{{ activeEntries.length }}</span> }\n </button>\n }\n <button type=\"button\" class=\"h-10 rounded-xl border border-slate-200 px-4 text-sm text-slate-600 hover:bg-slate-50\" (click)=\"clear()\">{{ config.labels?.clear || '\u041E\u0447\u0438\u0441\u0442\u0438\u0442\u0438' }}</button>\n @if (resultCount !== undefined) { <span class=\"w-full text-xs text-slate-500\">\u0417\u043D\u0430\u0439\u0434\u0435\u043D\u043E {{ resultCount }}</span> }\n </div>\n\n @if (activeEntries.length) {\n <div class=\"mt-3 flex flex-wrap items-center gap-2\">\n @for (entry of activeEntries; track entry.id) {\n <button type=\"button\" class=\"inline-flex items-center gap-2 rounded-full bg-blue-50 px-3 py-1.5 text-xs text-blue-800\" (click)=\"remove(entry.id)\">{{ entry.text }} <span aria-hidden=\"true\">\u00D7</span></button>\n }\n <button type=\"button\" class=\"px-2 text-xs font-medium text-blue-600\" (click)=\"clear()\">\u0421\u043A\u0438\u043D\u0443\u0442\u0438 \u0432\u0441\u0435</button>\n </div>\n }\n</section>\n\n@if (advancedOpen) {\n <div class=\"fixed inset-0 z-[1100]\" (click)=\"advancedOpen = false\">\n <div class=\"absolute inset-0 bg-slate-900/10\"></div>\n <section class=\"absolute right-4 top-24 w-[min(42rem,calc(100vw-2rem))] rounded-2xl border border-slate-200 bg-white p-5 shadow-2xl\" (click)=\"$event.stopPropagation()\">\n <header class=\"mb-5 flex items-center justify-between\">\n <h2 class=\"text-base font-semibold text-slate-900\">{{ config.labels?.additionalFilters || '\u0414\u043E\u0434\u0430\u0442\u043A\u043E\u0432\u0456 \u0444\u0456\u043B\u044C\u0442\u0440\u0438' }}</h2>\n <button type=\"button\" class=\"text-slate-500\" (click)=\"advancedOpen = false\" aria-label=\"\u0417\u0430\u043A\u0440\u0438\u0442\u0438\">\u00D7</button>\n </header>\n <div class=\"grid gap-4 sm:grid-cols-2\">\n @for (field of advancedFields; track field.id) {\n @if (field.type === 'select' || field.type === 'boolean') {\n <input-select [label]=\"field.label\" [icon]=\"field.icon\" [data]=\"field.data || ''\" [options]=\"options(field)\" [multiple]=\"!!field.multiple\" [searchable]=\"!!field.data\" [placeholder]=\"field.placeholder || '\u041E\u0431\u0435\u0440\u0456\u0442\u044C\u2026'\" [ngModel]=\"fieldValue(field)\" (ngModelChange)=\"setField(field.id, $event)\" />\n } @else {\n <input-base [label]=\"field.label\" [icon]=\"field.icon\" [type]=\"field.type === 'number' ? 'number' : field.type === 'date' ? 'date' : 'text'\" [placeholder]=\"field.placeholder || ''\" [ngModel]=\"fieldValue(field)\" (ngModelChange)=\"setField(field.id, $event)\" />\n }\n }\n </div>\n <footer class=\"mt-6 flex flex-wrap items-center justify-between gap-2\">\n <button type=\"button\" class=\"text-sm font-medium text-blue-600\" (click)=\"clear()\">{{ config.labels?.reset || '\u0421\u043A\u0438\u043D\u0443\u0442\u0438' }}</button>\n <div class=\"flex gap-2\">\n <button type=\"button\" class=\"rounded-xl border border-slate-200 px-4 py-2 text-sm\" (click)=\"cancel()\">{{ config.labels?.cancel || '\u0421\u043A\u0430\u0441\u0443\u0432\u0430\u0442\u0438' }}</button>\n <button type=\"button\" class=\"rounded-xl bg-blue-600 px-5 py-2 text-sm font-semibold text-white hover:bg-blue-700\" (click)=\"apply()\">{{ config.labels?.apply || '\u0417\u0430\u0441\u0442\u043E\u0441\u0443\u0432\u0430\u0442\u0438' }}</button>\n </div>\n </footer>\n </section>\n </div>\n}\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: LucideDynamicIcon, selector: "svg[lucideIcon]", inputs: ["lucideIcon"] }, { kind: "component", type: InputComponent, selector: "input-base", inputs: ["label", "hint", "placeholder", "icon", "name", "id", "type", "element", "rows", "required", "autocomplete", "pattern", "error", "actions"], outputs: ["actionTriggered"] }, { kind: "component", type: InputSelectComponent, selector: "input-select", inputs: ["label", "hint", "error", "placeholder", "icon", "name", "data", "dataParams", "required", "disabled", "multiple", "emptyStateLabel", "searchable", "options", "triggerClass", "panelClass", "actions"], outputs: ["actionTriggered", "valueChange", "searchChange"] }] });
104
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: FilterPanelComponent, isStandalone: true, selector: "maro-filter-panel", inputs: { config: "config", value: "value" }, outputs: { valueChange: "valueChange" }, viewQueries: [{ propertyName: "selectInputs", predicate: InputSelectComponent, descendants: true }], usesOnChanges: true, ngImport: i0, template: "<section class=\"rounded-2xl border border-slate-200 bg-white p-4 shadow-sm\">\n <div class=\"flex flex-wrap items-end gap-3\">\n @if (config.search; as search) {\n <div class=\"min-w-[15rem] flex-[1.2]\">\n <input-base [icon]=\"search.icon || 'search'\" [placeholder]=\"search.placeholder || '\u041F\u043E\u0448\u0443\u043A\u2026'\" [ngModel]=\"draft[search.id] || ''\" (ngModelChange)=\"setSearch($event)\" />\n </div>\n }\n @for (field of quickFields; track field.id) {\n <div class=\"min-w-[10rem] flex-1\">\n @if (field.type === 'select' || field.type === 'boolean') {\n <input-select [name]=\"field.id\" [label]=\"field.label\" [icon]=\"field.icon\" [data]=\"field.data || ''\" [options]=\"options(field)\" [multiple]=\"!!field.multiple\" [searchable]=\"!!field.data\" [placeholder]=\"field.placeholder || '\u041E\u0431\u0435\u0440\u0456\u0442\u044C\u2026'\" [ngModel]=\"fieldValue(field)\" (ngModelChange)=\"setField(field.id, $event)\" />\n } @else {\n <input-base [label]=\"field.label\" [icon]=\"field.icon\" [type]=\"field.type === 'number' ? 'number' : field.type === 'date' ? 'date' : 'text'\" [placeholder]=\"field.placeholder || ''\" [ngModel]=\"fieldValue(field)\" (ngModelChange)=\"setField(field.id, $event)\" />\n }\n </div>\n }\n @if (advancedFields.length) {\n <button type=\"button\" class=\"flex h-10 items-center gap-2 rounded-lg border border-[#E5E7EB] bg-white px-4 text-sm font-semibold text-[#111827] transition hover:border-slate-300 hover:bg-slate-50 active:bg-slate-100\" (click)=\"toggleAdvanced()\">\n <svg [lucideIcon]=\"filtersIcon\" class=\"h-4 w-4\"></svg>{{ config.labels?.filters || '\u0424\u0456\u043B\u044C\u0442\u0440\u0438' }}\n @if (activeFilterCount) { <span class=\"rounded-full bg-[#0F172A] px-1.5 text-xs text-white\">{{ activeFilterCount }}</span> }\n </button>\n }\n <button type=\"button\" class=\"inline-flex h-10 items-center gap-2 rounded-lg border border-[#E5E7EB] bg-white px-4 text-sm font-semibold text-[#111827] transition hover:border-slate-300 hover:bg-slate-50 active:bg-slate-100\" (click)=\"clear()\">\n <svg [lucideIcon]=\"closeIcon\" class=\"h-4 w-4\"></svg>{{ config.labels?.clear || '\u041E\u0447\u0438\u0441\u0442\u0438\u0442\u0438' }}\n </button>\n </div>\n\n @if (activeEntries.length) {\n <div class=\"mt-3 flex flex-wrap items-center gap-2\">\n @for (entry of activeEntries; track entry.id) {\n <button type=\"button\" class=\"inline-flex items-center gap-2 rounded-full border border-[#E5E7EB] bg-slate-50 px-3 py-1.5 text-xs text-[#111827] transition hover:bg-slate-100\" (click)=\"remove(entry.id)\">{{ entry.text }} <svg [lucideIcon]=\"closeIcon\" class=\"h-3.5 w-3.5 text-slate-500\"></svg></button>\n }\n <button type=\"button\" class=\"px-2 text-xs font-semibold text-[#0F172A] hover:text-slate-600\" (click)=\"clear()\">\u0421\u043A\u0438\u043D\u0443\u0442\u0438 \u0432\u0441\u0435</button>\n </div>\n }\n</section>\n\n<div class=\"fixed inset-0 z-[1100]\" [class.hidden]=\"!advancedOpen\" [attr.aria-hidden]=\"!advancedOpen\" (click)=\"cancel()\">\n <div class=\"absolute inset-0 bg-slate-900/10\"></div>\n <section class=\"absolute right-4 top-24 w-[min(42rem,calc(100vw-2rem))] rounded-2xl border border-slate-200 bg-white p-5 shadow-2xl\" (click)=\"$event.stopPropagation()\">\n <header class=\"mb-5 flex items-center justify-between\">\n <h2 class=\"text-base font-semibold text-slate-900\">{{ config.labels?.additionalFilters || '\u0414\u043E\u0434\u0430\u0442\u043A\u043E\u0432\u0456 \u0444\u0456\u043B\u044C\u0442\u0440\u0438' }}</h2>\n <button type=\"button\" class=\"rounded-lg p-1 text-slate-500 transition hover:bg-slate-100 hover:text-[#111827]\" (click)=\"cancel()\" aria-label=\"\u0417\u0430\u043A\u0440\u0438\u0442\u0438\"><svg [lucideIcon]=\"closeIcon\" class=\"h-5 w-5\"></svg></button>\n </header>\n <div class=\"grid gap-4 sm:grid-cols-2\">\n @for (field of advancedFields; track field.id) {\n @if (field.type === 'select' || field.type === 'boolean') {\n <input-select [name]=\"field.id\" [label]=\"field.label\" [icon]=\"field.icon\" [data]=\"field.data || ''\" [options]=\"options(field)\" [multiple]=\"!!field.multiple\" [searchable]=\"!!field.data\" [placeholder]=\"field.placeholder || '\u041E\u0431\u0435\u0440\u0456\u0442\u044C\u2026'\" [ngModel]=\"fieldValue(field)\" (ngModelChange)=\"setField(field.id, $event)\" />\n } @else {\n <input-base [label]=\"field.label\" [icon]=\"field.icon\" [type]=\"field.type === 'number' ? 'number' : field.type === 'date' ? 'date' : 'text'\" [placeholder]=\"field.placeholder || ''\" [ngModel]=\"fieldValue(field)\" (ngModelChange)=\"setField(field.id, $event)\" />\n }\n }\n </div>\n <footer class=\"mt-6 flex flex-wrap items-center justify-between gap-2\">\n <button type=\"button\" class=\"text-sm font-semibold text-[#0F172A] hover:text-slate-600\" (click)=\"clear()\">{{ config.labels?.reset || '\u0421\u043A\u0438\u043D\u0443\u0442\u0438' }}</button>\n <button type=\"button\" class=\"rounded-lg bg-[#0F172A] px-4 py-2 text-sm font-semibold text-white transition hover:bg-slate-800 active:bg-slate-900\" (click)=\"apply()\">{{ config.labels?.apply || '\u0413\u043E\u0442\u043E\u0432\u043E' }}</button>\n </footer>\n </section>\n </div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: LucideDynamicIcon, selector: "svg[lucideIcon]", inputs: ["lucideIcon"] }, { kind: "component", type: InputComponent, selector: "input-base", inputs: ["label", "hint", "placeholder", "icon", "name", "id", "type", "element", "rows", "required", "autocomplete", "pattern", "error", "actions"], outputs: ["actionTriggered"] }, { kind: "component", type: InputSelectComponent, selector: "input-select", inputs: ["label", "hint", "error", "placeholder", "icon", "name", "data", "dataParams", "required", "disabled", "multiple", "emptyStateLabel", "searchable", "options", "triggerClass", "panelClass", "actions"], outputs: ["actionTriggered", "valueChange", "searchChange"] }] });
103
105
  }
104
106
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: FilterPanelComponent, decorators: [{
105
107
  type: Component,
106
- args: [{ selector: 'maro-filter-panel', standalone: true, imports: [CommonModule, FormsModule, LucideDynamicIcon, InputComponent, InputSelectComponent], template: "<section class=\"rounded-2xl border border-slate-200 bg-white p-4 shadow-sm\">\n <div class=\"flex flex-wrap items-end gap-3\">\n @if (config.search; as search) {\n <div class=\"min-w-[15rem] flex-[1.2]\">\n <input-base [icon]=\"search.icon || 'search'\" [placeholder]=\"search.placeholder || '\u041F\u043E\u0448\u0443\u043A\u2026'\" [ngModel]=\"draft[search.id] || ''\" (ngModelChange)=\"setSearch($event)\" />\n </div>\n }\n @for (field of quickFields; track field.id) {\n <div class=\"min-w-[10rem] flex-1\">\n @if (field.type === 'select' || field.type === 'boolean') {\n <input-select [label]=\"field.label\" [icon]=\"field.icon\" [data]=\"field.data || ''\" [options]=\"options(field)\" [multiple]=\"!!field.multiple\" [searchable]=\"!!field.data\" [placeholder]=\"field.placeholder || '\u041E\u0431\u0435\u0440\u0456\u0442\u044C\u2026'\" [ngModel]=\"fieldValue(field)\" (ngModelChange)=\"setField(field.id, $event)\" />\n } @else {\n <input-base [label]=\"field.label\" [icon]=\"field.icon\" [type]=\"field.type === 'number' ? 'number' : field.type === 'date' ? 'date' : 'text'\" [placeholder]=\"field.placeholder || ''\" [ngModel]=\"fieldValue(field)\" (ngModelChange)=\"setField(field.id, $event)\" />\n }\n </div>\n }\n @if (advancedFields.length) {\n <button type=\"button\" class=\"flex h-10 items-center gap-2 rounded-xl border border-blue-300 px-4 text-sm font-semibold text-blue-600 hover:bg-blue-50\" (click)=\"advancedOpen = !advancedOpen\">\n <svg [lucideIcon]=\"'sliders-horizontal'\" class=\"h-4 w-4\"></svg>{{ config.labels?.filters || '\u0424\u0456\u043B\u044C\u0442\u0440\u0438' }}\n @if (activeEntries.length) { <span class=\"rounded-full bg-blue-100 px-1.5 text-xs\">{{ activeEntries.length }}</span> }\n </button>\n }\n <button type=\"button\" class=\"h-10 rounded-xl border border-slate-200 px-4 text-sm text-slate-600 hover:bg-slate-50\" (click)=\"clear()\">{{ config.labels?.clear || '\u041E\u0447\u0438\u0441\u0442\u0438\u0442\u0438' }}</button>\n @if (resultCount !== undefined) { <span class=\"w-full text-xs text-slate-500\">\u0417\u043D\u0430\u0439\u0434\u0435\u043D\u043E {{ resultCount }}</span> }\n </div>\n\n @if (activeEntries.length) {\n <div class=\"mt-3 flex flex-wrap items-center gap-2\">\n @for (entry of activeEntries; track entry.id) {\n <button type=\"button\" class=\"inline-flex items-center gap-2 rounded-full bg-blue-50 px-3 py-1.5 text-xs text-blue-800\" (click)=\"remove(entry.id)\">{{ entry.text }} <span aria-hidden=\"true\">\u00D7</span></button>\n }\n <button type=\"button\" class=\"px-2 text-xs font-medium text-blue-600\" (click)=\"clear()\">\u0421\u043A\u0438\u043D\u0443\u0442\u0438 \u0432\u0441\u0435</button>\n </div>\n }\n</section>\n\n@if (advancedOpen) {\n <div class=\"fixed inset-0 z-[1100]\" (click)=\"advancedOpen = false\">\n <div class=\"absolute inset-0 bg-slate-900/10\"></div>\n <section class=\"absolute right-4 top-24 w-[min(42rem,calc(100vw-2rem))] rounded-2xl border border-slate-200 bg-white p-5 shadow-2xl\" (click)=\"$event.stopPropagation()\">\n <header class=\"mb-5 flex items-center justify-between\">\n <h2 class=\"text-base font-semibold text-slate-900\">{{ config.labels?.additionalFilters || '\u0414\u043E\u0434\u0430\u0442\u043A\u043E\u0432\u0456 \u0444\u0456\u043B\u044C\u0442\u0440\u0438' }}</h2>\n <button type=\"button\" class=\"text-slate-500\" (click)=\"advancedOpen = false\" aria-label=\"\u0417\u0430\u043A\u0440\u0438\u0442\u0438\">\u00D7</button>\n </header>\n <div class=\"grid gap-4 sm:grid-cols-2\">\n @for (field of advancedFields; track field.id) {\n @if (field.type === 'select' || field.type === 'boolean') {\n <input-select [label]=\"field.label\" [icon]=\"field.icon\" [data]=\"field.data || ''\" [options]=\"options(field)\" [multiple]=\"!!field.multiple\" [searchable]=\"!!field.data\" [placeholder]=\"field.placeholder || '\u041E\u0431\u0435\u0440\u0456\u0442\u044C\u2026'\" [ngModel]=\"fieldValue(field)\" (ngModelChange)=\"setField(field.id, $event)\" />\n } @else {\n <input-base [label]=\"field.label\" [icon]=\"field.icon\" [type]=\"field.type === 'number' ? 'number' : field.type === 'date' ? 'date' : 'text'\" [placeholder]=\"field.placeholder || ''\" [ngModel]=\"fieldValue(field)\" (ngModelChange)=\"setField(field.id, $event)\" />\n }\n }\n </div>\n <footer class=\"mt-6 flex flex-wrap items-center justify-between gap-2\">\n <button type=\"button\" class=\"text-sm font-medium text-blue-600\" (click)=\"clear()\">{{ config.labels?.reset || '\u0421\u043A\u0438\u043D\u0443\u0442\u0438' }}</button>\n <div class=\"flex gap-2\">\n <button type=\"button\" class=\"rounded-xl border border-slate-200 px-4 py-2 text-sm\" (click)=\"cancel()\">{{ config.labels?.cancel || '\u0421\u043A\u0430\u0441\u0443\u0432\u0430\u0442\u0438' }}</button>\n <button type=\"button\" class=\"rounded-xl bg-blue-600 px-5 py-2 text-sm font-semibold text-white hover:bg-blue-700\" (click)=\"apply()\">{{ config.labels?.apply || '\u0417\u0430\u0441\u0442\u043E\u0441\u0443\u0432\u0430\u0442\u0438' }}</button>\n </div>\n </footer>\n </section>\n </div>\n}\n" }]
107
- }], propDecorators: { config: [{
108
+ args: [{ selector: 'maro-filter-panel', standalone: true, imports: [CommonModule, FormsModule, LucideDynamicIcon, InputComponent, InputSelectComponent], template: "<section class=\"rounded-2xl border border-slate-200 bg-white p-4 shadow-sm\">\n <div class=\"flex flex-wrap items-end gap-3\">\n @if (config.search; as search) {\n <div class=\"min-w-[15rem] flex-[1.2]\">\n <input-base [icon]=\"search.icon || 'search'\" [placeholder]=\"search.placeholder || '\u041F\u043E\u0448\u0443\u043A\u2026'\" [ngModel]=\"draft[search.id] || ''\" (ngModelChange)=\"setSearch($event)\" />\n </div>\n }\n @for (field of quickFields; track field.id) {\n <div class=\"min-w-[10rem] flex-1\">\n @if (field.type === 'select' || field.type === 'boolean') {\n <input-select [name]=\"field.id\" [label]=\"field.label\" [icon]=\"field.icon\" [data]=\"field.data || ''\" [options]=\"options(field)\" [multiple]=\"!!field.multiple\" [searchable]=\"!!field.data\" [placeholder]=\"field.placeholder || '\u041E\u0431\u0435\u0440\u0456\u0442\u044C\u2026'\" [ngModel]=\"fieldValue(field)\" (ngModelChange)=\"setField(field.id, $event)\" />\n } @else {\n <input-base [label]=\"field.label\" [icon]=\"field.icon\" [type]=\"field.type === 'number' ? 'number' : field.type === 'date' ? 'date' : 'text'\" [placeholder]=\"field.placeholder || ''\" [ngModel]=\"fieldValue(field)\" (ngModelChange)=\"setField(field.id, $event)\" />\n }\n </div>\n }\n @if (advancedFields.length) {\n <button type=\"button\" class=\"flex h-10 items-center gap-2 rounded-lg border border-[#E5E7EB] bg-white px-4 text-sm font-semibold text-[#111827] transition hover:border-slate-300 hover:bg-slate-50 active:bg-slate-100\" (click)=\"toggleAdvanced()\">\n <svg [lucideIcon]=\"filtersIcon\" class=\"h-4 w-4\"></svg>{{ config.labels?.filters || '\u0424\u0456\u043B\u044C\u0442\u0440\u0438' }}\n @if (activeFilterCount) { <span class=\"rounded-full bg-[#0F172A] px-1.5 text-xs text-white\">{{ activeFilterCount }}</span> }\n </button>\n }\n <button type=\"button\" class=\"inline-flex h-10 items-center gap-2 rounded-lg border border-[#E5E7EB] bg-white px-4 text-sm font-semibold text-[#111827] transition hover:border-slate-300 hover:bg-slate-50 active:bg-slate-100\" (click)=\"clear()\">\n <svg [lucideIcon]=\"closeIcon\" class=\"h-4 w-4\"></svg>{{ config.labels?.clear || '\u041E\u0447\u0438\u0441\u0442\u0438\u0442\u0438' }}\n </button>\n </div>\n\n @if (activeEntries.length) {\n <div class=\"mt-3 flex flex-wrap items-center gap-2\">\n @for (entry of activeEntries; track entry.id) {\n <button type=\"button\" class=\"inline-flex items-center gap-2 rounded-full border border-[#E5E7EB] bg-slate-50 px-3 py-1.5 text-xs text-[#111827] transition hover:bg-slate-100\" (click)=\"remove(entry.id)\">{{ entry.text }} <svg [lucideIcon]=\"closeIcon\" class=\"h-3.5 w-3.5 text-slate-500\"></svg></button>\n }\n <button type=\"button\" class=\"px-2 text-xs font-semibold text-[#0F172A] hover:text-slate-600\" (click)=\"clear()\">\u0421\u043A\u0438\u043D\u0443\u0442\u0438 \u0432\u0441\u0435</button>\n </div>\n }\n</section>\n\n<div class=\"fixed inset-0 z-[1100]\" [class.hidden]=\"!advancedOpen\" [attr.aria-hidden]=\"!advancedOpen\" (click)=\"cancel()\">\n <div class=\"absolute inset-0 bg-slate-900/10\"></div>\n <section class=\"absolute right-4 top-24 w-[min(42rem,calc(100vw-2rem))] rounded-2xl border border-slate-200 bg-white p-5 shadow-2xl\" (click)=\"$event.stopPropagation()\">\n <header class=\"mb-5 flex items-center justify-between\">\n <h2 class=\"text-base font-semibold text-slate-900\">{{ config.labels?.additionalFilters || '\u0414\u043E\u0434\u0430\u0442\u043A\u043E\u0432\u0456 \u0444\u0456\u043B\u044C\u0442\u0440\u0438' }}</h2>\n <button type=\"button\" class=\"rounded-lg p-1 text-slate-500 transition hover:bg-slate-100 hover:text-[#111827]\" (click)=\"cancel()\" aria-label=\"\u0417\u0430\u043A\u0440\u0438\u0442\u0438\"><svg [lucideIcon]=\"closeIcon\" class=\"h-5 w-5\"></svg></button>\n </header>\n <div class=\"grid gap-4 sm:grid-cols-2\">\n @for (field of advancedFields; track field.id) {\n @if (field.type === 'select' || field.type === 'boolean') {\n <input-select [name]=\"field.id\" [label]=\"field.label\" [icon]=\"field.icon\" [data]=\"field.data || ''\" [options]=\"options(field)\" [multiple]=\"!!field.multiple\" [searchable]=\"!!field.data\" [placeholder]=\"field.placeholder || '\u041E\u0431\u0435\u0440\u0456\u0442\u044C\u2026'\" [ngModel]=\"fieldValue(field)\" (ngModelChange)=\"setField(field.id, $event)\" />\n } @else {\n <input-base [label]=\"field.label\" [icon]=\"field.icon\" [type]=\"field.type === 'number' ? 'number' : field.type === 'date' ? 'date' : 'text'\" [placeholder]=\"field.placeholder || ''\" [ngModel]=\"fieldValue(field)\" (ngModelChange)=\"setField(field.id, $event)\" />\n }\n }\n </div>\n <footer class=\"mt-6 flex flex-wrap items-center justify-between gap-2\">\n <button type=\"button\" class=\"text-sm font-semibold text-[#0F172A] hover:text-slate-600\" (click)=\"clear()\">{{ config.labels?.reset || '\u0421\u043A\u0438\u043D\u0443\u0442\u0438' }}</button>\n <button type=\"button\" class=\"rounded-lg bg-[#0F172A] px-4 py-2 text-sm font-semibold text-white transition hover:bg-slate-800 active:bg-slate-900\" (click)=\"apply()\">{{ config.labels?.apply || '\u0413\u043E\u0442\u043E\u0432\u043E' }}</button>\n </footer>\n </section>\n </div>\n" }]
109
+ }], propDecorators: { selectInputs: [{
110
+ type: ViewChildren,
111
+ args: [InputSelectComponent]
112
+ }], config: [{
108
113
  type: Input,
109
114
  args: [{ required: true }]
110
115
  }], value: [{
111
116
  type: Input
112
- }], resultCount: [{
113
- type: Input
114
117
  }], valueChange: [{
115
118
  type: Output
116
119
  }] } });
@@ -1 +1 @@
1
- {"version":3,"file":"maro-tech-filters.mjs","sources":["../../../src/lib/filter-panel/filter-panel.component.ts","../../../src/lib/filter-panel/filter-panel.component.html","../../../src/maro-tech-filters.ts"],"sourcesContent":["import { CommonModule } from '@angular/common';\nimport { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core';\nimport { FormsModule } from '@angular/forms';\nimport { LucideDynamicIcon } from '@lucide/angular';\nimport { InputComponent, InputSelectComponent } from '@maro-tech/form';\nimport { FilterFieldConfig, FilterPanelConfig, FilterState, FilterValue } from './filter-panel.models';\n\n@Component({\n selector: 'maro-filter-panel',\n standalone: true,\n imports: [CommonModule, FormsModule, LucideDynamicIcon, InputComponent, InputSelectComponent],\n templateUrl: './filter-panel.component.html',\n})\nexport class FilterPanelComponent implements OnChanges {\n @Input({ required: true }) config!: FilterPanelConfig;\n @Input() value: FilterState = {};\n @Input() resultCount?: number;\n @Output() valueChange = new EventEmitter<FilterState>();\n\n draft: FilterState = {};\n advancedOpen = false;\n private searchTimer?: ReturnType<typeof setTimeout>;\n\n get quickFields(): FilterFieldConfig[] {\n return (this.config?.fields ?? []).filter((field) => field.placement === 'quick');\n }\n\n get advancedFields(): FilterFieldConfig[] {\n return (this.config?.fields ?? []).filter((field) => field.placement !== 'quick');\n }\n\n get activeEntries(): Array<{ id: string; label: string; text: string }> {\n const fields = this.config?.fields ?? [];\n return Object.entries(this.value ?? {}).flatMap(([id, raw]) => {\n if (raw === '' || raw === null || raw === undefined || (Array.isArray(raw) && !raw.length)) return [];\n if (id === this.config?.search?.id) return [{ id, label: '', text: `Пошук: ${String(raw)}` }];\n const field = fields.find((item) => item.id === id);\n if (!field) return [];\n const values = Array.isArray(raw) ? raw : [raw];\n const labels = values.map((entry) => field.options?.find((option) => String(option.value) === String(entry))?.label ?? String(entry));\n return [{ id, label: field.label, text: `${field.label}: ${labels.join(', ')}` }];\n });\n }\n\n ngOnChanges(changes: SimpleChanges): void {\n if (changes['value']) this.draft = { ...(this.value ?? {}) };\n }\n\n setField(id: string, value: FilterValue): void {\n this.draft = { ...this.draft, [id]: value };\n const field = this.config.fields.find((item) => item.id === id);\n if (field?.placement === 'quick') {\n const next = { ...this.value, [id]: value };\n this.valueChange.emit(next);\n }\n }\n\n setSearch(value: string): void {\n const id = this.config.search?.id;\n if (!id) return;\n this.draft = { ...this.draft, [id]: value };\n if (this.searchTimer) clearTimeout(this.searchTimer);\n this.searchTimer = setTimeout(() => {\n this.valueChange.emit({ ...this.value, [id]: value });\n }, 300);\n }\n\n apply(): void {\n if (this.searchTimer) clearTimeout(this.searchTimer);\n this.searchTimer = undefined;\n this.valueChange.emit({ ...this.draft });\n this.advancedOpen = false;\n }\n\n cancel(): void {\n this.draft = { ...(this.value ?? {}) };\n this.advancedOpen = false;\n }\n\n toggleAdvanced(): void {\n this.advancedOpen = !this.advancedOpen;\n if (this.advancedOpen) this.draft = { ...(this.value ?? {}) };\n }\n\n clear(): void {\n if (this.searchTimer) clearTimeout(this.searchTimer);\n this.searchTimer = undefined;\n this.draft = {};\n this.valueChange.emit({});\n this.advancedOpen = false;\n }\n\n remove(id: string): void {\n const next = { ...this.value };\n delete next[id];\n this.draft = { ...next };\n this.valueChange.emit(next);\n }\n\n fieldValue(field: FilterFieldConfig): FilterValue {\n return this.draft[field.id] ?? (field.multiple ? [] : '');\n }\n\n options(field: FilterFieldConfig): Array<{ label: string; value: string | null }> {\n return (field.options ?? []).map((option) => ({ label: option.label, value: String(option.value) }));\n }\n\n ngOnDestroy(): void {\n if (this.searchTimer) clearTimeout(this.searchTimer);\n }\n}\n","<section class=\"rounded-2xl border border-slate-200 bg-white p-4 shadow-sm\">\n <div class=\"flex flex-wrap items-end gap-3\">\n @if (config.search; as search) {\n <div class=\"min-w-[15rem] flex-[1.2]\">\n <input-base [icon]=\"search.icon || 'search'\" [placeholder]=\"search.placeholder || 'Пошук…'\" [ngModel]=\"draft[search.id] || ''\" (ngModelChange)=\"setSearch($event)\" />\n </div>\n }\n @for (field of quickFields; track field.id) {\n <div class=\"min-w-[10rem] flex-1\">\n @if (field.type === 'select' || field.type === 'boolean') {\n <input-select [label]=\"field.label\" [icon]=\"field.icon\" [data]=\"field.data || ''\" [options]=\"options(field)\" [multiple]=\"!!field.multiple\" [searchable]=\"!!field.data\" [placeholder]=\"field.placeholder || 'Оберіть…'\" [ngModel]=\"fieldValue(field)\" (ngModelChange)=\"setField(field.id, $event)\" />\n } @else {\n <input-base [label]=\"field.label\" [icon]=\"field.icon\" [type]=\"field.type === 'number' ? 'number' : field.type === 'date' ? 'date' : 'text'\" [placeholder]=\"field.placeholder || ''\" [ngModel]=\"fieldValue(field)\" (ngModelChange)=\"setField(field.id, $event)\" />\n }\n </div>\n }\n @if (advancedFields.length) {\n <button type=\"button\" class=\"flex h-10 items-center gap-2 rounded-xl border border-blue-300 px-4 text-sm font-semibold text-blue-600 hover:bg-blue-50\" (click)=\"advancedOpen = !advancedOpen\">\n <svg [lucideIcon]=\"'sliders-horizontal'\" class=\"h-4 w-4\"></svg>{{ config.labels?.filters || 'Фільтри' }}\n @if (activeEntries.length) { <span class=\"rounded-full bg-blue-100 px-1.5 text-xs\">{{ activeEntries.length }}</span> }\n </button>\n }\n <button type=\"button\" class=\"h-10 rounded-xl border border-slate-200 px-4 text-sm text-slate-600 hover:bg-slate-50\" (click)=\"clear()\">{{ config.labels?.clear || 'Очистити' }}</button>\n @if (resultCount !== undefined) { <span class=\"w-full text-xs text-slate-500\">Знайдено {{ resultCount }}</span> }\n </div>\n\n @if (activeEntries.length) {\n <div class=\"mt-3 flex flex-wrap items-center gap-2\">\n @for (entry of activeEntries; track entry.id) {\n <button type=\"button\" class=\"inline-flex items-center gap-2 rounded-full bg-blue-50 px-3 py-1.5 text-xs text-blue-800\" (click)=\"remove(entry.id)\">{{ entry.text }} <span aria-hidden=\"true\">×</span></button>\n }\n <button type=\"button\" class=\"px-2 text-xs font-medium text-blue-600\" (click)=\"clear()\">Скинути все</button>\n </div>\n }\n</section>\n\n@if (advancedOpen) {\n <div class=\"fixed inset-0 z-[1100]\" (click)=\"advancedOpen = false\">\n <div class=\"absolute inset-0 bg-slate-900/10\"></div>\n <section class=\"absolute right-4 top-24 w-[min(42rem,calc(100vw-2rem))] rounded-2xl border border-slate-200 bg-white p-5 shadow-2xl\" (click)=\"$event.stopPropagation()\">\n <header class=\"mb-5 flex items-center justify-between\">\n <h2 class=\"text-base font-semibold text-slate-900\">{{ config.labels?.additionalFilters || 'Додаткові фільтри' }}</h2>\n <button type=\"button\" class=\"text-slate-500\" (click)=\"advancedOpen = false\" aria-label=\"Закрити\">×</button>\n </header>\n <div class=\"grid gap-4 sm:grid-cols-2\">\n @for (field of advancedFields; track field.id) {\n @if (field.type === 'select' || field.type === 'boolean') {\n <input-select [label]=\"field.label\" [icon]=\"field.icon\" [data]=\"field.data || ''\" [options]=\"options(field)\" [multiple]=\"!!field.multiple\" [searchable]=\"!!field.data\" [placeholder]=\"field.placeholder || 'Оберіть…'\" [ngModel]=\"fieldValue(field)\" (ngModelChange)=\"setField(field.id, $event)\" />\n } @else {\n <input-base [label]=\"field.label\" [icon]=\"field.icon\" [type]=\"field.type === 'number' ? 'number' : field.type === 'date' ? 'date' : 'text'\" [placeholder]=\"field.placeholder || ''\" [ngModel]=\"fieldValue(field)\" (ngModelChange)=\"setField(field.id, $event)\" />\n }\n }\n </div>\n <footer class=\"mt-6 flex flex-wrap items-center justify-between gap-2\">\n <button type=\"button\" class=\"text-sm font-medium text-blue-600\" (click)=\"clear()\">{{ config.labels?.reset || 'Скинути' }}</button>\n <div class=\"flex gap-2\">\n <button type=\"button\" class=\"rounded-xl border border-slate-200 px-4 py-2 text-sm\" (click)=\"cancel()\">{{ config.labels?.cancel || 'Скасувати' }}</button>\n <button type=\"button\" class=\"rounded-xl bg-blue-600 px-5 py-2 text-sm font-semibold text-white hover:bg-blue-700\" (click)=\"apply()\">{{ config.labels?.apply || 'Застосувати' }}</button>\n </div>\n </footer>\n </section>\n </div>\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"maro-tech-filters.mjs","sources":["../../../src/lib/filter-panel/filter-panel.component.ts","../../../src/lib/filter-panel/filter-panel.component.html","../../../src/maro-tech-filters.ts"],"sourcesContent":["import { CommonModule } from '@angular/common';\nimport { Component, EventEmitter, Input, OnChanges, Output, QueryList, SimpleChanges, ViewChildren } from '@angular/core';\nimport { FormsModule } from '@angular/forms';\nimport { LucideDynamicIcon, LucideSlidersHorizontal, LucideX } from '@lucide/angular';\nimport { InputComponent, InputSelectComponent } from '@maro-tech/form';\nimport { FilterFieldConfig, FilterPanelConfig, FilterState, FilterValue } from './filter-panel.models';\n\n@Component({\n selector: 'maro-filter-panel',\n standalone: true,\n imports: [CommonModule, FormsModule, LucideDynamicIcon, InputComponent, InputSelectComponent],\n templateUrl: './filter-panel.component.html',\n})\nexport class FilterPanelComponent implements OnChanges {\n @ViewChildren(InputSelectComponent) private selectInputs?: QueryList<InputSelectComponent>;\n @Input({ required: true }) config!: FilterPanelConfig;\n @Input() value: FilterState = {};\n @Output() valueChange = new EventEmitter<FilterState>();\n\n readonly filtersIcon = LucideSlidersHorizontal;\n readonly closeIcon = LucideX;\n draft: FilterState = {};\n advancedOpen = false;\n private searchTimer?: ReturnType<typeof setTimeout>;\n\n get quickFields(): FilterFieldConfig[] {\n return (this.config?.fields ?? []).filter((field) => field.placement === 'quick');\n }\n\n get advancedFields(): FilterFieldConfig[] {\n return (this.config?.fields ?? []).filter((field) => field.placement !== 'quick');\n }\n\n get activeFilterCount(): number {\n return (this.config?.fields ?? []).filter((field) => {\n const value = this.value?.[field.id];\n return value !== '' && value !== null && value !== undefined && (!Array.isArray(value) || value.length > 0);\n }).length;\n }\n\n get activeEntries(): Array<{ id: string; label: string; text: string }> {\n const fields = this.config?.fields ?? [];\n return Object.entries(this.value ?? {}).flatMap(([id, raw]) => {\n if (raw === '' || raw === null || raw === undefined || (Array.isArray(raw) && !raw.length)) return [];\n if (id === this.config?.search?.id) return [{ id, label: '', text: `Пошук: ${String(raw)}` }];\n const field = fields.find((item) => item.id === id);\n if (!field) return [];\n const values = Array.isArray(raw) ? raw : [raw];\n const selectedLabels = this.selectInputs?.find((input) => input.name === id)?.selectedLabelsByValue;\n const labels = values.map((entry) => field.options?.find((option) => String(option.value) === String(entry))?.label\n ?? selectedLabels?.[String(entry)]\n ?? String(entry));\n return [{ id, label: field.label, text: `${field.label}: ${labels.join(', ')}` }];\n });\n }\n\n ngOnChanges(changes: SimpleChanges): void {\n if (changes['value']) this.draft = { ...(this.value ?? {}) };\n }\n\n setField(id: string, value: FilterValue): void {\n this.draft = { ...this.draft, [id]: value };\n this.valueChange.emit({ ...this.value, [id]: value });\n }\n\n setSearch(value: string): void {\n const id = this.config.search?.id;\n if (!id) return;\n this.draft = { ...this.draft, [id]: value };\n if (this.searchTimer) clearTimeout(this.searchTimer);\n this.searchTimer = setTimeout(() => {\n this.valueChange.emit({ ...this.value, [id]: value });\n }, 300);\n }\n\n apply(): void {\n this.advancedOpen = false;\n }\n\n cancel(): void {\n this.advancedOpen = false;\n }\n\n toggleAdvanced(): void {\n this.advancedOpen = !this.advancedOpen;\n if (this.advancedOpen) this.draft = { ...(this.value ?? {}) };\n }\n\n clear(): void {\n if (this.searchTimer) clearTimeout(this.searchTimer);\n this.searchTimer = undefined;\n this.draft = {};\n this.valueChange.emit({});\n this.advancedOpen = false;\n }\n\n remove(id: string): void {\n const next = { ...this.value };\n delete next[id];\n this.draft = { ...next };\n this.valueChange.emit(next);\n }\n\n fieldValue(field: FilterFieldConfig): FilterValue {\n return this.draft[field.id] ?? (field.multiple ? [] : '');\n }\n\n options(field: FilterFieldConfig): Array<{ label: string; value: string | null }> {\n return (field.options ?? []).map((option) => ({ label: option.label, value: String(option.value) }));\n }\n\n ngOnDestroy(): void {\n if (this.searchTimer) clearTimeout(this.searchTimer);\n }\n}\n","<section class=\"rounded-2xl border border-slate-200 bg-white p-4 shadow-sm\">\n <div class=\"flex flex-wrap items-end gap-3\">\n @if (config.search; as search) {\n <div class=\"min-w-[15rem] flex-[1.2]\">\n <input-base [icon]=\"search.icon || 'search'\" [placeholder]=\"search.placeholder || 'Пошук…'\" [ngModel]=\"draft[search.id] || ''\" (ngModelChange)=\"setSearch($event)\" />\n </div>\n }\n @for (field of quickFields; track field.id) {\n <div class=\"min-w-[10rem] flex-1\">\n @if (field.type === 'select' || field.type === 'boolean') {\n <input-select [name]=\"field.id\" [label]=\"field.label\" [icon]=\"field.icon\" [data]=\"field.data || ''\" [options]=\"options(field)\" [multiple]=\"!!field.multiple\" [searchable]=\"!!field.data\" [placeholder]=\"field.placeholder || 'Оберіть…'\" [ngModel]=\"fieldValue(field)\" (ngModelChange)=\"setField(field.id, $event)\" />\n } @else {\n <input-base [label]=\"field.label\" [icon]=\"field.icon\" [type]=\"field.type === 'number' ? 'number' : field.type === 'date' ? 'date' : 'text'\" [placeholder]=\"field.placeholder || ''\" [ngModel]=\"fieldValue(field)\" (ngModelChange)=\"setField(field.id, $event)\" />\n }\n </div>\n }\n @if (advancedFields.length) {\n <button type=\"button\" class=\"flex h-10 items-center gap-2 rounded-lg border border-[#E5E7EB] bg-white px-4 text-sm font-semibold text-[#111827] transition hover:border-slate-300 hover:bg-slate-50 active:bg-slate-100\" (click)=\"toggleAdvanced()\">\n <svg [lucideIcon]=\"filtersIcon\" class=\"h-4 w-4\"></svg>{{ config.labels?.filters || 'Фільтри' }}\n @if (activeFilterCount) { <span class=\"rounded-full bg-[#0F172A] px-1.5 text-xs text-white\">{{ activeFilterCount }}</span> }\n </button>\n }\n <button type=\"button\" class=\"inline-flex h-10 items-center gap-2 rounded-lg border border-[#E5E7EB] bg-white px-4 text-sm font-semibold text-[#111827] transition hover:border-slate-300 hover:bg-slate-50 active:bg-slate-100\" (click)=\"clear()\">\n <svg [lucideIcon]=\"closeIcon\" class=\"h-4 w-4\"></svg>{{ config.labels?.clear || 'Очистити' }}\n </button>\n </div>\n\n @if (activeEntries.length) {\n <div class=\"mt-3 flex flex-wrap items-center gap-2\">\n @for (entry of activeEntries; track entry.id) {\n <button type=\"button\" class=\"inline-flex items-center gap-2 rounded-full border border-[#E5E7EB] bg-slate-50 px-3 py-1.5 text-xs text-[#111827] transition hover:bg-slate-100\" (click)=\"remove(entry.id)\">{{ entry.text }} <svg [lucideIcon]=\"closeIcon\" class=\"h-3.5 w-3.5 text-slate-500\"></svg></button>\n }\n <button type=\"button\" class=\"px-2 text-xs font-semibold text-[#0F172A] hover:text-slate-600\" (click)=\"clear()\">Скинути все</button>\n </div>\n }\n</section>\n\n<div class=\"fixed inset-0 z-[1100]\" [class.hidden]=\"!advancedOpen\" [attr.aria-hidden]=\"!advancedOpen\" (click)=\"cancel()\">\n <div class=\"absolute inset-0 bg-slate-900/10\"></div>\n <section class=\"absolute right-4 top-24 w-[min(42rem,calc(100vw-2rem))] rounded-2xl border border-slate-200 bg-white p-5 shadow-2xl\" (click)=\"$event.stopPropagation()\">\n <header class=\"mb-5 flex items-center justify-between\">\n <h2 class=\"text-base font-semibold text-slate-900\">{{ config.labels?.additionalFilters || 'Додаткові фільтри' }}</h2>\n <button type=\"button\" class=\"rounded-lg p-1 text-slate-500 transition hover:bg-slate-100 hover:text-[#111827]\" (click)=\"cancel()\" aria-label=\"Закрити\"><svg [lucideIcon]=\"closeIcon\" class=\"h-5 w-5\"></svg></button>\n </header>\n <div class=\"grid gap-4 sm:grid-cols-2\">\n @for (field of advancedFields; track field.id) {\n @if (field.type === 'select' || field.type === 'boolean') {\n <input-select [name]=\"field.id\" [label]=\"field.label\" [icon]=\"field.icon\" [data]=\"field.data || ''\" [options]=\"options(field)\" [multiple]=\"!!field.multiple\" [searchable]=\"!!field.data\" [placeholder]=\"field.placeholder || 'Оберіть…'\" [ngModel]=\"fieldValue(field)\" (ngModelChange)=\"setField(field.id, $event)\" />\n } @else {\n <input-base [label]=\"field.label\" [icon]=\"field.icon\" [type]=\"field.type === 'number' ? 'number' : field.type === 'date' ? 'date' : 'text'\" [placeholder]=\"field.placeholder || ''\" [ngModel]=\"fieldValue(field)\" (ngModelChange)=\"setField(field.id, $event)\" />\n }\n }\n </div>\n <footer class=\"mt-6 flex flex-wrap items-center justify-between gap-2\">\n <button type=\"button\" class=\"text-sm font-semibold text-[#0F172A] hover:text-slate-600\" (click)=\"clear()\">{{ config.labels?.reset || 'Скинути' }}</button>\n <button type=\"button\" class=\"rounded-lg bg-[#0F172A] px-4 py-2 text-sm font-semibold text-white transition hover:bg-slate-800 active:bg-slate-900\" (click)=\"apply()\">{{ config.labels?.apply || 'Готово' }}</button>\n </footer>\n </section>\n </div>\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maro-tech/filters",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "private": false,
5
5
  "peerDependencies": {
6
6
  "@angular/common": "^21.2.0",
@@ -8,7 +8,7 @@
8
8
  "@angular/forms": "^21.2.0",
9
9
  "@lucide/angular": "^1.48.0",
10
10
  "@maro-tech/core": "*",
11
- "@maro-tech/form": "^0.2.9",
11
+ "@maro-tech/form": "^0.2.11",
12
12
  "rxjs": "~7.8.0"
13
13
  },
14
14
  "dependencies": {
@@ -1,5 +1,6 @@
1
1
  import * as i0 from '@angular/core';
2
2
  import { OnChanges, EventEmitter, SimpleChanges } from '@angular/core';
3
+ import { LucideSlidersHorizontal, LucideX } from '@lucide/angular';
3
4
 
4
5
  type FilterValue = string | string[] | number | boolean | null;
5
6
  type FilterState = Record<string, FilterValue>;
@@ -32,7 +33,6 @@ interface FilterPanelConfig {
32
33
  filters?: string;
33
34
  additionalFilters?: string;
34
35
  apply?: string;
35
- cancel?: string;
36
36
  clear?: string;
37
37
  reset?: string;
38
38
  saveSet?: string;
@@ -41,15 +41,18 @@ interface FilterPanelConfig {
41
41
  }
42
42
 
43
43
  declare class FilterPanelComponent implements OnChanges {
44
+ private selectInputs?;
44
45
  config: FilterPanelConfig;
45
46
  value: FilterState;
46
- resultCount?: number;
47
47
  valueChange: EventEmitter<FilterState>;
48
+ readonly filtersIcon: typeof LucideSlidersHorizontal;
49
+ readonly closeIcon: typeof LucideX;
48
50
  draft: FilterState;
49
51
  advancedOpen: boolean;
50
52
  private searchTimer?;
51
53
  get quickFields(): FilterFieldConfig[];
52
54
  get advancedFields(): FilterFieldConfig[];
55
+ get activeFilterCount(): number;
53
56
  get activeEntries(): Array<{
54
57
  id: string;
55
58
  label: string;
@@ -70,7 +73,7 @@ declare class FilterPanelComponent implements OnChanges {
70
73
  }>;
71
74
  ngOnDestroy(): void;
72
75
  static ɵfac: i0.ɵɵFactoryDeclaration<FilterPanelComponent, never>;
73
- static ɵcmp: i0.ɵɵComponentDeclaration<FilterPanelComponent, "maro-filter-panel", never, { "config": { "alias": "config"; "required": true; }; "value": { "alias": "value"; "required": false; }; "resultCount": { "alias": "resultCount"; "required": false; }; }, { "valueChange": "valueChange"; }, never, never, true, never>;
76
+ static ɵcmp: i0.ɵɵComponentDeclaration<FilterPanelComponent, "maro-filter-panel", never, { "config": { "alias": "config"; "required": true; }; "value": { "alias": "value"; "required": false; }; }, { "valueChange": "valueChange"; }, never, never, true, never>;
74
77
  }
75
78
 
76
79
  export { FilterPanelComponent };