@sinequa/atomic-angular 2.3.0 → 2.4.1

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.
@@ -3354,7 +3354,8 @@ const QUERY_PARAMS_DEFAULTS = {
3354
3354
  aggregations: void 0,
3355
3355
  orderBy: "",
3356
3356
  groupBy: "",
3357
- neuralSearch: void 0
3357
+ neuralSearch: void 0,
3358
+ aggregation_overrides: void 0
3358
3359
  };
3359
3360
  const QueryParamsStore = signalStore({ providedIn: "root" }, withDevtools("QueryParams"), withQueryParamsFeatures());
3360
3361
  function withQueryParamsFeatures() {
@@ -3578,7 +3579,7 @@ function withQueryParamsFeatures() {
3578
3579
  };
3579
3580
  },
3580
3581
  getQuery() {
3581
- const { filters: allFilters = [], sort, tab, basket, text: queryText = "", spellingCorrectionMode } = getState(store);
3582
+ const { filters: allFilters = [], sort, tab, basket, text: queryText = "", spellingCorrectionMode, aggregation_overrides } = getState(store);
3582
3583
  let { name } = getState(store);
3583
3584
  if (!name) name = (route.snapshot.firstChild?.children[0]?.data)?.["queryName"] || appStore.getDefaultQuery()?.name || "";
3584
3585
  let text = queryText;
@@ -3599,7 +3600,8 @@ function withQueryParamsFeatures() {
3599
3600
  spellingCorrectionMode,
3600
3601
  tab,
3601
3602
  text,
3602
- basket
3603
+ basket,
3604
+ aggregation_overrides
3603
3605
  }));
3604
3606
  }
3605
3607
  })));
@@ -9983,20 +9985,12 @@ var AdvancedSearchTabs = class AdvancedSearchTabs {
9983
9985
  executeSearch() {
9984
9986
  this.selectionStore.update({ queryText: this.queryText() });
9985
9987
  }
9986
- constructor(destroyRef) {
9987
- const host = inject(ElementRef).nativeElement;
9988
- const onKeydownCapture = (event) => {
9989
- if (event.key === "Enter") this.executeSearch();
9990
- };
9991
- host.addEventListener("keydown", onKeydownCapture, true);
9992
- destroyRef.onDestroy(() => host.removeEventListener("keydown", onKeydownCapture, true));
9993
- }
9994
9988
  static ɵfac = i0.ɵɵngDeclareFactory({
9995
9989
  minVersion: "12.0.0",
9996
9990
  version: "22.2.1",
9997
9991
  ngImport: i0,
9998
9992
  type: AdvancedSearchTabs,
9999
- deps: [{ token: i0.DestroyRef }],
9993
+ deps: [],
10000
9994
  target: i0.ɵɵFactoryTarget.Component
10001
9995
  });
10002
9996
  static ɵcmp = i0.ɵɵngDeclareComponent({
@@ -10032,24 +10026,12 @@ var AdvancedSearchTabs = class AdvancedSearchTabs {
10032
10026
  host: { classAttribute: "flex min-h-0 flex-col overflow-x-hidden text-(length:--size-xs)" },
10033
10027
  providers: [provideTranslocoScope("drawers")],
10034
10028
  ngImport: i0,
10035
- template: "@if (article()) {\n <section class=\"mx-3 mt-3 mb-3 flex shrink-0 flex-col gap-4\">\n <!-- Enter submits via the capture-phase listener in the component (see the constructor) — a\n bubble-phase (keydown.enter) here never sees it, Combobox's own input listener swallows it\n first. (search) still covers the click-the-send-button path. No debounce: same \"submit on\n Enter/send, not on every keystroke\" contract as the previous input-group implementation. -->\n <SearchBar\n size=\"small\"\n [placeholder]=\"'drawers.inputPlaceholder' | transloco\"\n [ariaLabel]=\"'drawers.inputPlaceholder' | transloco\"\n [(value)]=\"queryText\"\n (search)=\"executeSearch()\" />\n </section>\n\n <!-- flex-1/min-h-0: this is what lets the panel content below be the only thing that scrolls —\n the search bar/tab list above are shrink-0, never pushed off or scrolled themselves. -->\n <Tabs class=\"flex min-h-0 flex-1 flex-col\">\n <!-- noTruncate skips the default `truncate` branch (whose `has-[span]:[&>span]:truncate` would\n otherwise force this span back to single-line ellipsis, defeating the wrap below); overriding\n its own `min-w-fit` with `min-w-[auto]` restores the flex item's normal \"automatic minimum\n size\" instead — for content that can wrap, that floor is the longest word (min-content), not\n the full unwrapped label (max-content, what `fit-content` resolves to here with nothing\n constraining the available space). A single-word label has nothing to wrap, so min-content ==\n max-content and it can't be squeezed below its own readable width — the flex-shrink deficit\n falls on the multi-word labels that actually have room to give, which wrap onto the span's\n line-clamp-2 instead of being cut with an ellipsis (appropriate for a single-line pill, not\n for a tab bar where \"Similar documents\"/\"Match locations\" routinely don't fit on one line).\n h-9 (fixed, not h-auto) + items-center on every tab keeps the row visually even whether a\n given label wraps onto 2 lines or stays on 1. No horizontal scroll/truncation fallback here on\n purpose — see preview-actions.ts's [matchTriggerWidth]=\"false\" on the search popover: it's the\n popover itself that grows (shrink-to-fit, up to max-w-xl) to make room for this row instead of\n this row squeezing to fit a fixed popover width. -->\n <TabList\n class=\"mx-3 shrink-0\"\n variant=\"inner\"\n [selectedTab]=\"selectedTab()\"\n (selectedTabChange)=\"activeTab.set($event)\">\n <Tab\n variant=\"secondary\"\n size=\"sm\"\n value=\"extracts\"\n noTruncate\n class=\"h-9 min-w-[auto] items-center\">\n <span\n class=\"line-clamp-2 text-center leading-tight whitespace-normal\"\n >{{ \"extracts\" | transloco }}</span\n >\n </Tab>\n @if (entities.hasContent()) {\n <Tab\n variant=\"secondary\"\n size=\"sm\"\n value=\"entities\"\n noTruncate\n class=\"h-9 min-w-[auto] items-center\">\n <span\n class=\"line-clamp-2 text-center leading-tight whitespace-normal\"\n >{{ \"drawers.entitiesTab\" | transloco }}</span\n >\n </Tab>\n }\n @if (matchLocations.hasContent()) {\n <Tab\n variant=\"secondary\"\n size=\"sm\"\n value=\"matchlocations\"\n noTruncate\n class=\"h-9 min-w-[auto] items-center\">\n <span\n class=\"line-clamp-2 text-center leading-tight whitespace-normal\"\n >{{ \"column.matchlocations\" | transloco }}</span\n >\n </Tab>\n }\n @if (labels.hasContent()) {\n <Tab\n variant=\"secondary\"\n size=\"sm\"\n value=\"labels\"\n noTruncate\n class=\"h-9 min-w-[auto] items-center\">\n <span\n class=\"line-clamp-2 text-center leading-tight whitespace-normal\"\n >{{ \"labels\" | transloco }}</span\n >\n </Tab>\n }\n @if (similar.hasContent()) {\n <Tab\n variant=\"secondary\"\n size=\"sm\"\n value=\"similar\"\n noTruncate\n class=\"h-9 min-w-[auto] items-center\">\n <span\n class=\"line-clamp-2 text-center leading-tight whitespace-normal\"\n >{{ \"drawers.similarDocuments\" | transloco }}</span\n >\n </Tab>\n }\n </TabList>\n\n <!-- The single scroll container: each TabPanel is `absolute inset-0` (only the active one\n visible, per TabPanelComponent's own `[class.hidden]`), so they all share this one\n min-h-0/flex-1/overflow-y-auto region instead of the whole component scrolling. -->\n <div class=\"relative m-3 min-h-0 flex-1 scrollbar-thin overflow-y-auto\">\n <!-- Every TabPanel below cedes its own tab stop to its content (TabPanelComponent's `tabIndex`\n doc, ui/tabs/tabs.ts): the APG makes a tabpanel focusable only when it holds no focusable\n child, otherwise Tab from the tab lands on the empty container instead of the first row.\n `extracts` is always rendered (own empty/loading states, per the class doc above) so its\n tabindex is computed from whether it currently has any focusable row;\n `entities`/`matchlocations`/`labels`/`similar` are only ever mounted via `hasContent()`,\n which — per their `injectXState` — guarantees at least one row, so a static -1 is safe there. -->\n <TabPanel\n value=\"extracts\"\n [tabindex]=\"\n extracts.loading() || extracts.extracts().length === 0 ? 0 : -1\n \"\n class=\"absolute inset-0\">\n <article-extracts-tab-panel [state]=\"extracts\" />\n </TabPanel>\n @if (entities.hasContent()) {\n <TabPanel value=\"entities\" tabindex=\"-1\" class=\"absolute inset-0\">\n <article-entities-tab-panel [state]=\"entities\" />\n </TabPanel>\n }\n @if (matchLocations.hasContent()) {\n <TabPanel value=\"matchlocations\" tabindex=\"-1\" class=\"absolute inset-0\">\n <article-match-locations-tab-panel [state]=\"matchLocations\" />\n </TabPanel>\n }\n @if (labels.hasContent()) {\n <TabPanel value=\"labels\" tabindex=\"-1\" class=\"absolute inset-0\">\n <article-labels-tab-panel [article]=\"article()\" [state]=\"labels\" />\n </TabPanel>\n }\n @if (similar.hasContent()) {\n <TabPanel value=\"similar\" tabindex=\"-1\" class=\"absolute inset-0\">\n <article-similar-documents-tab-panel\n [article]=\"article()\"\n [strategy]=\"previewStrategy()\"\n [state]=\"similar\"\n (selected)=\"selected.emit($event)\" />\n </TabPanel>\n }\n </div>\n </Tabs>\n}\n",
10029
+ template: "@if (article()) {\n <section class=\"mx-3 mt-3 mb-3 flex shrink-0 flex-col gap-4\">\n <!-- Enter submits through (keydown.enter), the send button through (search). No debounce: same \"submit on\n Enter/send, not on every keystroke\" contract as the previous input-group implementation. -->\n <SearchBar\n size=\"small\"\n [placeholder]=\"'drawers.inputPlaceholder' | transloco\"\n [aria-label]=\"'drawers.inputPlaceholder' | transloco\"\n [(value)]=\"queryText\"\n (keydown.enter)=\"executeSearch()\"\n (search)=\"executeSearch()\" />\n </section>\n\n <!-- flex-1/min-h-0: this is what lets the panel content below be the only thing that scrolls —\n the search bar/tab list above are shrink-0, never pushed off or scrolled themselves. -->\n <Tabs class=\"flex min-h-0 flex-1 flex-col\">\n <!-- noTruncate skips the default `truncate` branch (whose `has-[span]:[&>span]:truncate` would\n otherwise force this span back to single-line ellipsis, defeating the wrap below); overriding\n its own `min-w-fit` with `min-w-[auto]` restores the flex item's normal \"automatic minimum\n size\" instead — for content that can wrap, that floor is the longest word (min-content), not\n the full unwrapped label (max-content, what `fit-content` resolves to here with nothing\n constraining the available space). A single-word label has nothing to wrap, so min-content ==\n max-content and it can't be squeezed below its own readable width — the flex-shrink deficit\n falls on the multi-word labels that actually have room to give, which wrap onto the span's\n line-clamp-2 instead of being cut with an ellipsis (appropriate for a single-line pill, not\n for a tab bar where \"Similar documents\"/\"Match locations\" routinely don't fit on one line).\n h-9 (fixed, not h-auto) + items-center on every tab keeps the row visually even whether a\n given label wraps onto 2 lines or stays on 1. No horizontal scroll/truncation fallback here on\n purpose — see preview-actions.ts's [matchTriggerWidth]=\"false\" on the search popover: it's the\n popover itself that grows (shrink-to-fit, up to max-w-xl) to make room for this row instead of\n this row squeezing to fit a fixed popover width. -->\n <TabList\n class=\"mx-3 shrink-0\"\n variant=\"inner\"\n [selectedTab]=\"selectedTab()\"\n (selectedTabChange)=\"activeTab.set($event)\">\n <Tab\n variant=\"secondary\"\n size=\"sm\"\n value=\"extracts\"\n noTruncate\n class=\"h-9 min-w-[auto] items-center\">\n <span\n class=\"line-clamp-2 text-center leading-tight whitespace-normal\"\n >{{ \"extracts\" | transloco }}</span\n >\n </Tab>\n @if (entities.hasContent()) {\n <Tab\n variant=\"secondary\"\n size=\"sm\"\n value=\"entities\"\n noTruncate\n class=\"h-9 min-w-[auto] items-center\">\n <span\n class=\"line-clamp-2 text-center leading-tight whitespace-normal\"\n >{{ \"drawers.entitiesTab\" | transloco }}</span\n >\n </Tab>\n }\n @if (matchLocations.hasContent()) {\n <Tab\n variant=\"secondary\"\n size=\"sm\"\n value=\"matchlocations\"\n noTruncate\n class=\"h-9 min-w-[auto] items-center\">\n <span\n class=\"line-clamp-2 text-center leading-tight whitespace-normal\"\n >{{ \"column.matchlocations\" | transloco }}</span\n >\n </Tab>\n }\n @if (labels.hasContent()) {\n <Tab\n variant=\"secondary\"\n size=\"sm\"\n value=\"labels\"\n noTruncate\n class=\"h-9 min-w-[auto] items-center\">\n <span\n class=\"line-clamp-2 text-center leading-tight whitespace-normal\"\n >{{ \"labels\" | transloco }}</span\n >\n </Tab>\n }\n @if (similar.hasContent()) {\n <Tab\n variant=\"secondary\"\n size=\"sm\"\n value=\"similar\"\n noTruncate\n class=\"h-9 min-w-[auto] items-center\">\n <span\n class=\"line-clamp-2 text-center leading-tight whitespace-normal\"\n >{{ \"drawers.similarDocuments\" | transloco }}</span\n >\n </Tab>\n }\n </TabList>\n\n <!-- The single scroll container: each TabPanel is `absolute inset-0` (only the active one\n visible, per TabPanelComponent's own `[class.hidden]`), so they all share this one\n min-h-0/flex-1/overflow-y-auto region instead of the whole component scrolling. -->\n <div class=\"relative m-3 min-h-0 flex-1 scrollbar-thin overflow-y-auto\">\n <!-- Every TabPanel below cedes its own tab stop to its content (TabPanelComponent's `tabIndex`\n doc, ui/tabs/tabs.ts): the APG makes a tabpanel focusable only when it holds no focusable\n child, otherwise Tab from the tab lands on the empty container instead of the first row.\n `extracts` is always rendered (own empty/loading states, per the class doc above) so its\n tabindex is computed from whether it currently has any focusable row;\n `entities`/`matchlocations`/`labels`/`similar` are only ever mounted via `hasContent()`,\n which — per their `injectXState` — guarantees at least one row, so a static -1 is safe there. -->\n <TabPanel\n value=\"extracts\"\n [tabindex]=\"\n extracts.loading() || extracts.extracts().length === 0 ? 0 : -1\n \"\n class=\"absolute inset-0\">\n <article-extracts-tab-panel [state]=\"extracts\" />\n </TabPanel>\n @if (entities.hasContent()) {\n <TabPanel value=\"entities\" tabindex=\"-1\" class=\"absolute inset-0\">\n <article-entities-tab-panel [state]=\"entities\" />\n </TabPanel>\n }\n @if (matchLocations.hasContent()) {\n <TabPanel value=\"matchlocations\" tabindex=\"-1\" class=\"absolute inset-0\">\n <article-match-locations-tab-panel [state]=\"matchLocations\" />\n </TabPanel>\n }\n @if (labels.hasContent()) {\n <TabPanel value=\"labels\" tabindex=\"-1\" class=\"absolute inset-0\">\n <article-labels-tab-panel [article]=\"article()\" [state]=\"labels\" />\n </TabPanel>\n }\n @if (similar.hasContent()) {\n <TabPanel value=\"similar\" tabindex=\"-1\" class=\"absolute inset-0\">\n <article-similar-documents-tab-panel\n [article]=\"article()\"\n [strategy]=\"previewStrategy()\"\n [state]=\"similar\"\n (selected)=\"selected.emit($event)\" />\n </TabPanel>\n }\n </div>\n </Tabs>\n}\n",
10036
10030
  dependencies: [
10037
10031
  {
10038
10032
  kind: "component",
10039
10033
  type: SearchBarComponent,
10040
- selector: "SearchBar, search-bar, searchbar",
10041
- inputs: [
10042
- "value",
10043
- "placeholder",
10044
- "ariaLabel",
10045
- "clearLabel",
10046
- "sendLabel",
10047
- "disabled",
10048
- "hotkey",
10049
- "size",
10050
- "class"
10051
- ],
10052
- outputs: ["valueChange", "search"]
10034
+ selector: "SearchBar:not([suggestions]), search-bar:not([suggestions]), searchbar:not([suggestions])"
10053
10035
  },
10054
10036
  {
10055
10037
  kind: "directive",
@@ -10147,10 +10129,9 @@ i0.ɵɵngDeclareClassMetadata({
10147
10129
  ArticleMatchLocationsTabPanel,
10148
10130
  ArticleSimilarDocumentsTabPanel
10149
10131
  ],
10150
- template: "@if (article()) {\n <section class=\"mx-3 mt-3 mb-3 flex shrink-0 flex-col gap-4\">\n <!-- Enter submits via the capture-phase listener in the component (see the constructor) — a\n bubble-phase (keydown.enter) here never sees it, Combobox's own input listener swallows it\n first. (search) still covers the click-the-send-button path. No debounce: same \"submit on\n Enter/send, not on every keystroke\" contract as the previous input-group implementation. -->\n <SearchBar\n size=\"small\"\n [placeholder]=\"'drawers.inputPlaceholder' | transloco\"\n [ariaLabel]=\"'drawers.inputPlaceholder' | transloco\"\n [(value)]=\"queryText\"\n (search)=\"executeSearch()\" />\n </section>\n\n <!-- flex-1/min-h-0: this is what lets the panel content below be the only thing that scrolls —\n the search bar/tab list above are shrink-0, never pushed off or scrolled themselves. -->\n <Tabs class=\"flex min-h-0 flex-1 flex-col\">\n <!-- noTruncate skips the default `truncate` branch (whose `has-[span]:[&>span]:truncate` would\n otherwise force this span back to single-line ellipsis, defeating the wrap below); overriding\n its own `min-w-fit` with `min-w-[auto]` restores the flex item's normal \"automatic minimum\n size\" instead — for content that can wrap, that floor is the longest word (min-content), not\n the full unwrapped label (max-content, what `fit-content` resolves to here with nothing\n constraining the available space). A single-word label has nothing to wrap, so min-content ==\n max-content and it can't be squeezed below its own readable width — the flex-shrink deficit\n falls on the multi-word labels that actually have room to give, which wrap onto the span's\n line-clamp-2 instead of being cut with an ellipsis (appropriate for a single-line pill, not\n for a tab bar where \"Similar documents\"/\"Match locations\" routinely don't fit on one line).\n h-9 (fixed, not h-auto) + items-center on every tab keeps the row visually even whether a\n given label wraps onto 2 lines or stays on 1. No horizontal scroll/truncation fallback here on\n purpose — see preview-actions.ts's [matchTriggerWidth]=\"false\" on the search popover: it's the\n popover itself that grows (shrink-to-fit, up to max-w-xl) to make room for this row instead of\n this row squeezing to fit a fixed popover width. -->\n <TabList\n class=\"mx-3 shrink-0\"\n variant=\"inner\"\n [selectedTab]=\"selectedTab()\"\n (selectedTabChange)=\"activeTab.set($event)\">\n <Tab\n variant=\"secondary\"\n size=\"sm\"\n value=\"extracts\"\n noTruncate\n class=\"h-9 min-w-[auto] items-center\">\n <span\n class=\"line-clamp-2 text-center leading-tight whitespace-normal\"\n >{{ \"extracts\" | transloco }}</span\n >\n </Tab>\n @if (entities.hasContent()) {\n <Tab\n variant=\"secondary\"\n size=\"sm\"\n value=\"entities\"\n noTruncate\n class=\"h-9 min-w-[auto] items-center\">\n <span\n class=\"line-clamp-2 text-center leading-tight whitespace-normal\"\n >{{ \"drawers.entitiesTab\" | transloco }}</span\n >\n </Tab>\n }\n @if (matchLocations.hasContent()) {\n <Tab\n variant=\"secondary\"\n size=\"sm\"\n value=\"matchlocations\"\n noTruncate\n class=\"h-9 min-w-[auto] items-center\">\n <span\n class=\"line-clamp-2 text-center leading-tight whitespace-normal\"\n >{{ \"column.matchlocations\" | transloco }}</span\n >\n </Tab>\n }\n @if (labels.hasContent()) {\n <Tab\n variant=\"secondary\"\n size=\"sm\"\n value=\"labels\"\n noTruncate\n class=\"h-9 min-w-[auto] items-center\">\n <span\n class=\"line-clamp-2 text-center leading-tight whitespace-normal\"\n >{{ \"labels\" | transloco }}</span\n >\n </Tab>\n }\n @if (similar.hasContent()) {\n <Tab\n variant=\"secondary\"\n size=\"sm\"\n value=\"similar\"\n noTruncate\n class=\"h-9 min-w-[auto] items-center\">\n <span\n class=\"line-clamp-2 text-center leading-tight whitespace-normal\"\n >{{ \"drawers.similarDocuments\" | transloco }}</span\n >\n </Tab>\n }\n </TabList>\n\n <!-- The single scroll container: each TabPanel is `absolute inset-0` (only the active one\n visible, per TabPanelComponent's own `[class.hidden]`), so they all share this one\n min-h-0/flex-1/overflow-y-auto region instead of the whole component scrolling. -->\n <div class=\"relative m-3 min-h-0 flex-1 scrollbar-thin overflow-y-auto\">\n <!-- Every TabPanel below cedes its own tab stop to its content (TabPanelComponent's `tabIndex`\n doc, ui/tabs/tabs.ts): the APG makes a tabpanel focusable only when it holds no focusable\n child, otherwise Tab from the tab lands on the empty container instead of the first row.\n `extracts` is always rendered (own empty/loading states, per the class doc above) so its\n tabindex is computed from whether it currently has any focusable row;\n `entities`/`matchlocations`/`labels`/`similar` are only ever mounted via `hasContent()`,\n which — per their `injectXState` — guarantees at least one row, so a static -1 is safe there. -->\n <TabPanel\n value=\"extracts\"\n [tabindex]=\"\n extracts.loading() || extracts.extracts().length === 0 ? 0 : -1\n \"\n class=\"absolute inset-0\">\n <article-extracts-tab-panel [state]=\"extracts\" />\n </TabPanel>\n @if (entities.hasContent()) {\n <TabPanel value=\"entities\" tabindex=\"-1\" class=\"absolute inset-0\">\n <article-entities-tab-panel [state]=\"entities\" />\n </TabPanel>\n }\n @if (matchLocations.hasContent()) {\n <TabPanel value=\"matchlocations\" tabindex=\"-1\" class=\"absolute inset-0\">\n <article-match-locations-tab-panel [state]=\"matchLocations\" />\n </TabPanel>\n }\n @if (labels.hasContent()) {\n <TabPanel value=\"labels\" tabindex=\"-1\" class=\"absolute inset-0\">\n <article-labels-tab-panel [article]=\"article()\" [state]=\"labels\" />\n </TabPanel>\n }\n @if (similar.hasContent()) {\n <TabPanel value=\"similar\" tabindex=\"-1\" class=\"absolute inset-0\">\n <article-similar-documents-tab-panel\n [article]=\"article()\"\n [strategy]=\"previewStrategy()\"\n [state]=\"similar\"\n (selected)=\"selected.emit($event)\" />\n </TabPanel>\n }\n </div>\n </Tabs>\n}\n"
10132
+ template: "@if (article()) {\n <section class=\"mx-3 mt-3 mb-3 flex shrink-0 flex-col gap-4\">\n <!-- Enter submits through (keydown.enter), the send button through (search). No debounce: same \"submit on\n Enter/send, not on every keystroke\" contract as the previous input-group implementation. -->\n <SearchBar\n size=\"small\"\n [placeholder]=\"'drawers.inputPlaceholder' | transloco\"\n [aria-label]=\"'drawers.inputPlaceholder' | transloco\"\n [(value)]=\"queryText\"\n (keydown.enter)=\"executeSearch()\"\n (search)=\"executeSearch()\" />\n </section>\n\n <!-- flex-1/min-h-0: this is what lets the panel content below be the only thing that scrolls —\n the search bar/tab list above are shrink-0, never pushed off or scrolled themselves. -->\n <Tabs class=\"flex min-h-0 flex-1 flex-col\">\n <!-- noTruncate skips the default `truncate` branch (whose `has-[span]:[&>span]:truncate` would\n otherwise force this span back to single-line ellipsis, defeating the wrap below); overriding\n its own `min-w-fit` with `min-w-[auto]` restores the flex item's normal \"automatic minimum\n size\" instead — for content that can wrap, that floor is the longest word (min-content), not\n the full unwrapped label (max-content, what `fit-content` resolves to here with nothing\n constraining the available space). A single-word label has nothing to wrap, so min-content ==\n max-content and it can't be squeezed below its own readable width — the flex-shrink deficit\n falls on the multi-word labels that actually have room to give, which wrap onto the span's\n line-clamp-2 instead of being cut with an ellipsis (appropriate for a single-line pill, not\n for a tab bar where \"Similar documents\"/\"Match locations\" routinely don't fit on one line).\n h-9 (fixed, not h-auto) + items-center on every tab keeps the row visually even whether a\n given label wraps onto 2 lines or stays on 1. No horizontal scroll/truncation fallback here on\n purpose — see preview-actions.ts's [matchTriggerWidth]=\"false\" on the search popover: it's the\n popover itself that grows (shrink-to-fit, up to max-w-xl) to make room for this row instead of\n this row squeezing to fit a fixed popover width. -->\n <TabList\n class=\"mx-3 shrink-0\"\n variant=\"inner\"\n [selectedTab]=\"selectedTab()\"\n (selectedTabChange)=\"activeTab.set($event)\">\n <Tab\n variant=\"secondary\"\n size=\"sm\"\n value=\"extracts\"\n noTruncate\n class=\"h-9 min-w-[auto] items-center\">\n <span\n class=\"line-clamp-2 text-center leading-tight whitespace-normal\"\n >{{ \"extracts\" | transloco }}</span\n >\n </Tab>\n @if (entities.hasContent()) {\n <Tab\n variant=\"secondary\"\n size=\"sm\"\n value=\"entities\"\n noTruncate\n class=\"h-9 min-w-[auto] items-center\">\n <span\n class=\"line-clamp-2 text-center leading-tight whitespace-normal\"\n >{{ \"drawers.entitiesTab\" | transloco }}</span\n >\n </Tab>\n }\n @if (matchLocations.hasContent()) {\n <Tab\n variant=\"secondary\"\n size=\"sm\"\n value=\"matchlocations\"\n noTruncate\n class=\"h-9 min-w-[auto] items-center\">\n <span\n class=\"line-clamp-2 text-center leading-tight whitespace-normal\"\n >{{ \"column.matchlocations\" | transloco }}</span\n >\n </Tab>\n }\n @if (labels.hasContent()) {\n <Tab\n variant=\"secondary\"\n size=\"sm\"\n value=\"labels\"\n noTruncate\n class=\"h-9 min-w-[auto] items-center\">\n <span\n class=\"line-clamp-2 text-center leading-tight whitespace-normal\"\n >{{ \"labels\" | transloco }}</span\n >\n </Tab>\n }\n @if (similar.hasContent()) {\n <Tab\n variant=\"secondary\"\n size=\"sm\"\n value=\"similar\"\n noTruncate\n class=\"h-9 min-w-[auto] items-center\">\n <span\n class=\"line-clamp-2 text-center leading-tight whitespace-normal\"\n >{{ \"drawers.similarDocuments\" | transloco }}</span\n >\n </Tab>\n }\n </TabList>\n\n <!-- The single scroll container: each TabPanel is `absolute inset-0` (only the active one\n visible, per TabPanelComponent's own `[class.hidden]`), so they all share this one\n min-h-0/flex-1/overflow-y-auto region instead of the whole component scrolling. -->\n <div class=\"relative m-3 min-h-0 flex-1 scrollbar-thin overflow-y-auto\">\n <!-- Every TabPanel below cedes its own tab stop to its content (TabPanelComponent's `tabIndex`\n doc, ui/tabs/tabs.ts): the APG makes a tabpanel focusable only when it holds no focusable\n child, otherwise Tab from the tab lands on the empty container instead of the first row.\n `extracts` is always rendered (own empty/loading states, per the class doc above) so its\n tabindex is computed from whether it currently has any focusable row;\n `entities`/`matchlocations`/`labels`/`similar` are only ever mounted via `hasContent()`,\n which — per their `injectXState` — guarantees at least one row, so a static -1 is safe there. -->\n <TabPanel\n value=\"extracts\"\n [tabindex]=\"\n extracts.loading() || extracts.extracts().length === 0 ? 0 : -1\n \"\n class=\"absolute inset-0\">\n <article-extracts-tab-panel [state]=\"extracts\" />\n </TabPanel>\n @if (entities.hasContent()) {\n <TabPanel value=\"entities\" tabindex=\"-1\" class=\"absolute inset-0\">\n <article-entities-tab-panel [state]=\"entities\" />\n </TabPanel>\n }\n @if (matchLocations.hasContent()) {\n <TabPanel value=\"matchlocations\" tabindex=\"-1\" class=\"absolute inset-0\">\n <article-match-locations-tab-panel [state]=\"matchLocations\" />\n </TabPanel>\n }\n @if (labels.hasContent()) {\n <TabPanel value=\"labels\" tabindex=\"-1\" class=\"absolute inset-0\">\n <article-labels-tab-panel [article]=\"article()\" [state]=\"labels\" />\n </TabPanel>\n }\n @if (similar.hasContent()) {\n <TabPanel value=\"similar\" tabindex=\"-1\" class=\"absolute inset-0\">\n <article-similar-documents-tab-panel\n [article]=\"article()\"\n [strategy]=\"previewStrategy()\"\n [state]=\"similar\"\n (selected)=\"selected.emit($event)\" />\n </TabPanel>\n }\n </div>\n </Tabs>\n}\n"
10151
10133
  }]
10152
10134
  }],
10153
- ctorParameters: () => [{ type: i0.DestroyRef }],
10154
10135
  propDecorators: {
10155
10136
  article: [{
10156
10137
  type: i0.Input,
@@ -10821,7 +10802,8 @@ var PreviewActionsComponent = class PreviewActionsComponent {
10821
10802
  "offset",
10822
10803
  "config",
10823
10804
  "lockPosition",
10824
- "matchTriggerWidth"
10805
+ "matchTriggerWidth",
10806
+ "hideWhenTriggerHidden"
10825
10807
  ],
10826
10808
  exportAs: ["popover", "dropdown"]
10827
10809
  },
@@ -20468,6 +20450,7 @@ var FeatureFlagsDialogComponent = class FeatureFlagsDialogComponent {
20468
20450
  "aria-label",
20469
20451
  "aria-labelledby",
20470
20452
  "presentational",
20453
+ "compact",
20471
20454
  "tabIndex"
20472
20455
  ],
20473
20456
  outputs: ["checkedChange"]
@@ -22849,6 +22832,7 @@ var AggregationDateComponent = class AggregationDateComponent {
22849
22832
  onClear = output();
22850
22833
  selectionChange = output();
22851
22834
  filtersChange = output();
22835
+ aggregationOverridesChange = output();
22852
22836
  isCollapsed = linkedSignal(() => this.collapsed(), ...ngDevMode ? [{ debugName: "isCollapsed" }] : /* istanbul ignore next */ []);
22853
22837
  expanded = computed(() => this.isCollapsed() ? null : "", ...ngDevMode ? [{ debugName: "expanded" }] : /* istanbul ignore next */ []);
22854
22838
  searchText = model("", ...ngDevMode ? [{ debugName: "searchText" }] : /* istanbul ignore next */ []);
@@ -22972,15 +22956,58 @@ var AggregationDateComponent = class AggregationDateComponent {
22972
22956
  kind: "option",
22973
22957
  option: this.optionToItemValue(option) ?? option
22974
22958
  } : this.toRangeSelection(filter));
22959
+ this.aggregationOverridesChange.emit(this.overridesWithMask(this.selectedMask()));
22975
22960
  this.filtersChange.emit(legacyFilterToFilter(filter));
22976
22961
  }
22962
+ selectedMask() {
22963
+ const option = this.dateModel().option;
22964
+ if (!option || option === "custom-range") return void 0;
22965
+ return this.dateOptions().find((o) => o.display === option)?.mask;
22966
+ }
22967
+ currentOverrides() {
22968
+ return this.isControlled() ? this.controlledQuery()?.aggregation_overrides : this.queryParamsStore.aggregation_overrides?.();
22969
+ }
22970
+ overridesWithMask(mask) {
22971
+ const key = this.aggregation()?.name ?? this.name();
22972
+ const overrides = { ...this.currentOverrides() };
22973
+ if (key) {
22974
+ if (mask) overrides[key] = { mask };
22975
+ else delete overrides[key];
22976
+ }
22977
+ return Object.keys(overrides).length > 0 ? overrides : void 0;
22978
+ }
22979
+ storeMask(mask) {
22980
+ const next = this.overridesWithMask(mask);
22981
+ if (JSON.stringify(next) === JSON.stringify(this.currentOverrides())) return;
22982
+ this.queryParamsStore.patch({ aggregation_overrides: next }, void 0, { syncUrl: false });
22983
+ }
22984
+ maskedLabel(option) {
22985
+ if (!option.mask || !option.value || option.display !== option.value) return void 0;
22986
+ const [year, month] = option.value.split("-").map(Number);
22987
+ const format = option.mask === "YYYY" ? { year: "numeric" } : {
22988
+ year: "numeric",
22989
+ month: "long"
22990
+ };
22991
+ try {
22992
+ return new Intl.DateTimeFormat(this.lang(), {
22993
+ ...format,
22994
+ timeZone: "UTC"
22995
+ }).format(new Date(Date.UTC(year, (month ?? 1) - 1, 1)));
22996
+ } catch {
22997
+ return;
22998
+ }
22999
+ }
22977
23000
  select() {
22978
23001
  this.selection.set(true);
22979
23002
  }
22980
23003
  apply() {
22981
23004
  try {
22982
23005
  if (this.isControlled()) this.emitCurrentSelection();
22983
- else this.queryParamsStore.updateFilter(this.getFormValueFilter(), void 0, { syncUrl: this.syncUrl() });
23006
+ else {
23007
+ const filter = this.getFormValueFilter();
23008
+ this.storeMask(this.selectedMask());
23009
+ this.queryParamsStore.updateFilter(filter, void 0, { syncUrl: this.syncUrl() });
23010
+ }
22984
23011
  this.selection.set(false);
22985
23012
  this.onApply.emit();
22986
23013
  } catch (err) {
@@ -23000,8 +23027,12 @@ var AggregationDateComponent = class AggregationDateComponent {
23000
23027
  if (agg && notify) {
23001
23028
  if (this.isControlled()) {
23002
23029
  this.selectionChange.emit({ kind: "range" });
23030
+ this.aggregationOverridesChange.emit(this.overridesWithMask(void 0));
23003
23031
  this.filtersChange.emit(void 0);
23004
- } else this.queryParamsStore.removeFilterByName(agg.name, void 0, { syncUrl: this.syncUrl() });
23032
+ } else {
23033
+ this.storeMask(void 0);
23034
+ this.queryParamsStore.removeFilterByName(agg.name, void 0, { syncUrl: this.syncUrl() });
23035
+ }
23005
23036
  this.onClear.emit();
23006
23037
  }
23007
23038
  }
@@ -23060,8 +23091,8 @@ var AggregationDateComponent = class AggregationDateComponent {
23060
23091
  this.clear(false);
23061
23092
  return;
23062
23093
  }
23063
- const { operator, value } = filter;
23064
- const code = this.dateOptions().find((option) => option.operator === operator && option.value === value)?.display ?? "custom-range";
23094
+ const { operator } = filter;
23095
+ const code = this.findPresetCode(filter) ?? "custom-range";
23065
23096
  let from = null, to = null;
23066
23097
  if (code === "custom-range") switch (operator) {
23067
23098
  case "lte":
@@ -23074,12 +23105,7 @@ var AggregationDateComponent = class AggregationDateComponent {
23074
23105
  from = filter.start;
23075
23106
  to = filter.end;
23076
23107
  break;
23077
- case "and": {
23078
- const filters = filter.filters;
23079
- from = filters?.find((f) => f.operator === "gte")?.value ?? null;
23080
- to = filters?.find((f) => f.operator === "lte")?.value ?? null;
23081
- break;
23082
- }
23108
+ case "and": ({from, to} = this.toBounds(filter.filters));
23083
23109
  }
23084
23110
  const formValue = {
23085
23111
  option: code,
@@ -23090,6 +23116,29 @@ var AggregationDateComponent = class AggregationDateComponent {
23090
23116
  };
23091
23117
  this.setModel(formValue);
23092
23118
  }
23119
+ findPresetCode(filter) {
23120
+ const options = this.dateOptions();
23121
+ const exact = options.find((option) => option.operator === filter.operator && option.value === filter.value);
23122
+ if (exact) return exact.display;
23123
+ if (filter.operator !== "and") return void 0;
23124
+ const { from, to } = this.toBounds(filter.filters);
23125
+ if (!from || !to) return void 0;
23126
+ return options.find((option) => {
23127
+ if (option.operator !== "and") return false;
23128
+ const bounds = this.toBounds(option.filters);
23129
+ return bounds.from === from && bounds.to === to;
23130
+ })?.display;
23131
+ }
23132
+ toBounds(filters) {
23133
+ const bound = (...operators) => {
23134
+ const value = filters?.find((f) => operators.includes(f.operator))?.value;
23135
+ return value === void 0 || value === null ? null : String(value);
23136
+ };
23137
+ return {
23138
+ from: bound("gte"),
23139
+ to: bound("lte", "lt")
23140
+ };
23141
+ }
23093
23142
  updateFormFromSelection(sel) {
23094
23143
  if (sel?.kind === "option") {
23095
23144
  const code = this.itemValueToOption(sel.option);
@@ -23151,11 +23200,11 @@ var AggregationDateComponent = class AggregationDateComponent {
23151
23200
  to: filter.end
23152
23201
  };
23153
23202
  case "and": {
23154
- const filters = filter.filters ?? [];
23203
+ const { from, to } = this.toBounds(filter.filters);
23155
23204
  return {
23156
23205
  kind: "range",
23157
- from: filters.find((f) => f.operator === "gte")?.value,
23158
- to: filters.find((f) => f.operator === "lte")?.value
23206
+ from: from ?? void 0,
23207
+ to: to ?? void 0
23159
23208
  };
23160
23209
  }
23161
23210
  default: return {
@@ -23344,12 +23393,13 @@ var AggregationDateComponent = class AggregationDateComponent {
23344
23393
  onClear: "onClear",
23345
23394
  selectionChange: "selectionChange",
23346
23395
  filtersChange: "filtersChange",
23396
+ aggregationOverridesChange: "aggregationOverridesChange",
23347
23397
  searchText: "searchTextChange"
23348
23398
  },
23349
23399
  host: { properties: { "class": "cn(\"@container block\", isEmpty() && \"hidden\")" } },
23350
23400
  providers: [provideTranslocoScope("filters")],
23351
23401
  ngImport: i0,
23352
- template: "<!-- `<details>`/`<summary>` stay native (open/close, the `name`-based exclusive accordion). A facet is a\n disclosure, not a grid row: see `aggregation-panel.html` for the why (same pattern, duplicated here\n since this facet type doesn't go through `AggregationPanelComponent`). The `<summary>` itself is\n `tabindex=\"-1\"`: the header's buttons (apply, clear, chevron) are the keyboard stops, see the\n chevron below. -->\n<details [attr.open]=\"expanded()\" [attr.name]=\"id()\" class=\"group space-y-2\">\n <!-- scroll-mb-3/scroll-mt-16: see aggregation-panel-row.html for the full rationale\n (bottom margin against the container's edge, much bigger top margin to also clear\n <More>'s sticky facet-name search box when navigating ArrowUp). -->\n <summary\n tabindex=\"-1\"\n [class.cursor-pointer]=\"collapsible()\"\n [class]=\"rowStateClass\"\n class=\"m-0 flex h-8 w-full scroll-mt-16 scroll-mb-3 items-center gap-1 pl-1 font-semibold select-none\"\n (click)=\"onHeaderClick($event)\">\n <ng-content select=\"label\">\n @let icon = aggregation()?.icon;\n @if (icon) {\n <FaIcon [faClass]=\"icon\" class=\"mr-1 shrink-0\" />\n }\n <span class=\"grow\">{{\n aggregation()?.display | syslang | transloco\n }}</span>\n </ng-content>\n\n @if (hasFilters()) {\n @let clearLabel = \"filters.clearFilters\" | transloco;\n <button\n type=\"button\"\n variant=\"tertiary\"\n [iconOnly]=\"true\"\n size=\"sm\"\n class=\"shrink-0\"\n [aria-label]=\"clearLabel\"\n [tooltip]=\"clearLabel\"\n (click)=\"$event.stopPropagation(); clear()\">\n <filter-x-icon class=\"shrink-0\" />\n </button>\n }\n\n @if (\n (!isControlled() || emitOn() === \"apply\") &&\n selection() &&\n validSelection()\n ) {\n @let label = \"filters.apply\" | transloco;\n <button\n type=\"button\"\n variant=\"accent\"\n size=\"sm\"\n class=\"overflow-hidden\"\n [aria-label]=\"label\"\n [tooltip]=\"label\"\n (click)=\"$event.stopPropagation(); apply()\">\n <FilterIcon class=\"shrink-0\" />\n <span class=\"truncate\">{{ label }}</span>\n </button>\n }\n @if (collapsible()) {\n <!-- The `<summary>` is out of the tab order (`tabindex=\"-1\"`), so this button is the keyboard way to\n collapse the facet. Its click bubbles to the `<summary>`, whose handler does the toggle, and it\n exposes `aria-expanded`. -->\n <button\n type=\"button\"\n variant=\"tertiary\"\n [iconOnly]=\"true\"\n size=\"sm\"\n [aria-label]=\"'filters.toggle' | transloco\"\n [attr.aria-expanded]=\"!isCollapsed()\"\n [tooltip]=\"'filters.toggle' | transloco\"\n class=\"shrink-0 cursor-pointer [&_svg]:transition-transform [&_svg]:duration-150 group-open:[&_svg]:rotate-90\">\n <chevron-right class=\"shrink-0\" />\n </button>\n }\n </summary>\n\n <!-- content wrapper -->\n <!-- RGAA 11.5 / WCAG 1.3.1: the radios below all share one `name`, so they are one set\n and have to be exposed as one. `role=\"group\"` here rather than `role=\"radiogroup\"`\n on the list: a radiogroup owns its radios directly, and the <li> in between would be\n invalid owned children — the list would also stop announcing how many options it\n holds. The name is the aggregation's own title, the words the header already shows;\n with no title the group stays unnamed rather than carrying an empty label. -->\n @let groupName = aggregation()?.display;\n <form (keydown.enter)=\"onOptionEnter($event)\">\n <div\n role=\"group\"\n [attr.aria-label]=\"groupName ? (groupName | syslang | transloco) : null\">\n <ul\n class=\"flex max-h-(--scroll-height,20rem) snap-y snap-start scrollbar-thin flex-col gap-0.5 overflow-auto pt-2\"\n role=\"list\">\n @for (option of dateOptions(); track $index) {\n <li\n role=\"listitem\"\n [attr.aria-label]=\"option.display | syslang | transloco\"\n [class]=\"\n cn(\n itemStateClass,\n 'flex cursor-pointer items-center p-2 text-sm text-(--font-neutral-base) select-none',\n dateModel().option === option.display &&\n 'bg-(--bg-primary-lighter)',\n option.hidden && 'hidden',\n option.disabled && 'pointer-events-none opacity-40'\n )\n \"\n [attr.aria-hidden]=\"option.disabled\">\n <radio\n name=\"date-filter\"\n class=\"grow\"\n [checked]=\"dateModel().option === option.display\"\n [disabled]=\"option.disabled\"\n (checkedChange)=\"selectOption(option.display)\">\n {{ option.display | syslang | transloco }}\n </radio>\n </li>\n }\n\n @if (allowCustomRange) {\n <li\n role=\"listitem\"\n aria-label=\"open date range picker\"\n [class]=\"\n cn(\n itemStateClass,\n 'flex cursor-pointer items-center gap-(--spacing-xs) p-2 text-sm text-(--font-neutral-base) select-none',\n dateModel().option === 'custom-range' &&\n 'bg-(--bg-primary-lighter)'\n )\n \">\n <radio\n name=\"date-filter\"\n class=\"grow items-center\"\n [checked]=\"dateModel().option === 'custom-range'\"\n (checkedChange)=\"selectOption('custom-range')\">\n <div class=\"flex flex-wrap items-center gap-x-1 gap-y-1\">\n <div class=\"flex items-center gap-1\">\n <label\n [attr.for]=\"startId\"\n class=\"radio-label min-w-8 truncate\"\n >{{ \"filters.from\" | transloco }}</label\n >\n <input-group\n class=\"h-8 max-w-[18ch] min-w-[18ch] has-[[readonly]]:border-(--stroke-primary-muted)\">\n <input\n input-control\n [id]=\"startId\"\n name=\"start\"\n type=\"text\"\n readonly\n [value]=\"customRangeFrom()\"\n (click)=\"selectAndOpenDialog()\"\n (keydown.enter)=\"onRangeInputEnter($event)\" />\n </input-group>\n </div>\n <div class=\"flex items-center gap-1\">\n <label\n [attr.for]=\"endId\"\n class=\"radio-label min-w-8 truncate text-right\"\n >{{ \"filters.to\" | transloco }}</label\n >\n <input-group\n class=\"h-8 max-w-[18ch] min-w-[18ch] has-[[readonly]]:border-(--stroke-primary-muted)\">\n <input\n input-control\n [id]=\"endId\"\n name=\"end\"\n type=\"text\"\n readonly\n [value]=\"customRangeTo()\"\n (click)=\"selectAndOpenDialog()\"\n (keydown.enter)=\"onRangeInputEnter($event)\" />\n </input-group>\n </div>\n </div>\n </radio>\n </li>\n }\n </ul>\n </div>\n </form>\n</details>\n",
23402
+ template: "<!-- `<details>`/`<summary>` stay native (open/close, the `name`-based exclusive accordion). A facet is a\n disclosure, not a grid row: see `aggregation-panel.html` for the why (same pattern, duplicated here\n since this facet type doesn't go through `AggregationPanelComponent`). The `<summary>` itself is\n `tabindex=\"-1\"`: the header's buttons (apply, clear, chevron) are the keyboard stops, see the\n chevron below. -->\n<details [attr.open]=\"expanded()\" [attr.name]=\"id()\" class=\"group space-y-2\">\n <!-- scroll-mb-3/scroll-mt-16: see aggregation-panel-row.html for the full rationale\n (bottom margin against the container's edge, much bigger top margin to also clear\n <More>'s sticky facet-name search box when navigating ArrowUp). -->\n <summary\n tabindex=\"-1\"\n [class.cursor-pointer]=\"collapsible()\"\n [class]=\"rowStateClass\"\n class=\"m-0 flex h-8 w-full scroll-mt-16 scroll-mb-3 items-center gap-1 pl-1 font-semibold select-none\"\n (click)=\"onHeaderClick($event)\">\n <ng-content select=\"label\">\n @let icon = aggregation()?.icon;\n @if (icon) {\n <FaIcon [faClass]=\"icon\" class=\"mr-1 shrink-0\" />\n }\n <span class=\"grow\">{{\n aggregation()?.display | syslang | transloco\n }}</span>\n </ng-content>\n\n @if (hasFilters()) {\n @let clearLabel = \"filters.clearFilters\" | transloco;\n <button\n type=\"button\"\n variant=\"tertiary\"\n [iconOnly]=\"true\"\n size=\"sm\"\n class=\"shrink-0\"\n [aria-label]=\"clearLabel\"\n [tooltip]=\"clearLabel\"\n (click)=\"$event.stopPropagation(); clear()\">\n <filter-x-icon class=\"shrink-0\" />\n </button>\n }\n\n @if (\n (!isControlled() || emitOn() === \"apply\") &&\n selection() &&\n validSelection()\n ) {\n @let label = \"filters.apply\" | transloco;\n <button\n type=\"button\"\n variant=\"accent\"\n size=\"sm\"\n class=\"overflow-hidden\"\n [aria-label]=\"label\"\n [tooltip]=\"label\"\n (click)=\"$event.stopPropagation(); apply()\">\n <FilterIcon class=\"shrink-0\" />\n <span class=\"truncate\">{{ label }}</span>\n </button>\n }\n @if (collapsible()) {\n <!-- The `<summary>` is out of the tab order (`tabindex=\"-1\"`), so this button is the keyboard way to\n collapse the facet. Its click bubbles to the `<summary>`, whose handler does the toggle, and it\n exposes `aria-expanded`. -->\n <button\n type=\"button\"\n variant=\"tertiary\"\n [iconOnly]=\"true\"\n size=\"sm\"\n [aria-label]=\"'filters.toggle' | transloco\"\n [attr.aria-expanded]=\"!isCollapsed()\"\n [tooltip]=\"'filters.toggle' | transloco\"\n class=\"shrink-0 cursor-pointer [&_svg]:transition-transform [&_svg]:duration-150 group-open:[&_svg]:rotate-90\">\n <chevron-right class=\"shrink-0\" />\n </button>\n }\n </summary>\n\n <!-- content wrapper -->\n <!-- RGAA 11.5 / WCAG 1.3.1: the radios below all share one `name`, so they are one set\n and have to be exposed as one. `role=\"group\"` here rather than `role=\"radiogroup\"`\n on the list: a radiogroup owns its radios directly, and the <li> in between would be\n invalid owned children — the list would also stop announcing how many options it\n holds. The name is the aggregation's own title, the words the header already shows;\n with no title the group stays unnamed rather than carrying an empty label. -->\n @let groupName = aggregation()?.display;\n <form (keydown.enter)=\"onOptionEnter($event)\">\n <div\n role=\"group\"\n [attr.aria-label]=\"groupName ? (groupName | syslang | transloco) : null\">\n <ul\n class=\"flex max-h-(--scroll-height,20rem) snap-y snap-start scrollbar-thin flex-col gap-0.5 overflow-auto pt-2\"\n role=\"list\">\n @for (option of dateOptions(); track option.display) {\n <li\n role=\"listitem\"\n [attr.aria-label]=\"\n maskedLabel(option) ?? (option.display | syslang | transloco)\n \"\n [class]=\"\n cn(\n itemStateClass,\n 'flex cursor-pointer items-center p-2 text-sm text-(--font-neutral-base) select-none',\n dateModel().option === option.display &&\n 'bg-(--bg-primary-lighter)',\n option.hidden && 'hidden',\n option.disabled && 'pointer-events-none opacity-40'\n )\n \"\n [attr.aria-hidden]=\"option.disabled\">\n <radio\n name=\"date-filter\"\n class=\"grow\"\n [checked]=\"dateModel().option === option.display\"\n [disabled]=\"option.disabled\"\n (checkedChange)=\"selectOption(option.display)\">\n {{\n maskedLabel(option) ?? (option.display | syslang | transloco)\n }}\n </radio>\n </li>\n }\n\n @if (allowCustomRange) {\n <li\n role=\"listitem\"\n aria-label=\"open date range picker\"\n [class]=\"\n cn(\n itemStateClass,\n 'flex cursor-pointer items-center gap-(--spacing-xs) p-2 text-sm text-(--font-neutral-base) select-none',\n dateModel().option === 'custom-range' &&\n 'bg-(--bg-primary-lighter)'\n )\n \">\n <radio\n name=\"date-filter\"\n class=\"grow items-center\"\n [checked]=\"dateModel().option === 'custom-range'\"\n (checkedChange)=\"selectOption('custom-range')\">\n <div class=\"flex flex-wrap items-center gap-x-1 gap-y-1\">\n <div class=\"flex items-center gap-1\">\n <label\n [attr.for]=\"startId\"\n class=\"radio-label min-w-8 truncate\"\n >{{ \"filters.from\" | transloco }}</label\n >\n <input-group\n class=\"h-8 max-w-[18ch] min-w-[18ch] has-[[readonly]]:border-(--stroke-primary-muted)\">\n <input\n input-control\n [id]=\"startId\"\n name=\"start\"\n type=\"text\"\n readonly\n [value]=\"customRangeFrom()\"\n (click)=\"selectAndOpenDialog()\"\n (keydown.enter)=\"onRangeInputEnter($event)\" />\n </input-group>\n </div>\n <div class=\"flex items-center gap-1\">\n <label\n [attr.for]=\"endId\"\n class=\"radio-label min-w-8 truncate text-right\"\n >{{ \"filters.to\" | transloco }}</label\n >\n <input-group\n class=\"h-8 max-w-[18ch] min-w-[18ch] has-[[readonly]]:border-(--stroke-primary-muted)\">\n <input\n input-control\n [id]=\"endId\"\n name=\"end\"\n type=\"text\"\n readonly\n [value]=\"customRangeTo()\"\n (click)=\"selectAndOpenDialog()\"\n (keydown.enter)=\"onRangeInputEnter($event)\" />\n </input-group>\n </div>\n </div>\n </radio>\n </li>\n }\n </ul>\n </div>\n </form>\n</details>\n",
23353
23403
  styles: [":host{min-width:200px}ul[role=list]{scrollbar-width:thin}\n"],
23354
23404
  dependencies: [
23355
23405
  {
@@ -23484,7 +23534,7 @@ i0.ɵɵngDeclareClassMetadata({
23484
23534
  TooltipDirective
23485
23535
  ],
23486
23536
  host: { "[class]": "cn(\"@container block\", isEmpty() && \"hidden\")" },
23487
- template: "<!-- `<details>`/`<summary>` stay native (open/close, the `name`-based exclusive accordion). A facet is a\n disclosure, not a grid row: see `aggregation-panel.html` for the why (same pattern, duplicated here\n since this facet type doesn't go through `AggregationPanelComponent`). The `<summary>` itself is\n `tabindex=\"-1\"`: the header's buttons (apply, clear, chevron) are the keyboard stops, see the\n chevron below. -->\n<details [attr.open]=\"expanded()\" [attr.name]=\"id()\" class=\"group space-y-2\">\n <!-- scroll-mb-3/scroll-mt-16: see aggregation-panel-row.html for the full rationale\n (bottom margin against the container's edge, much bigger top margin to also clear\n <More>'s sticky facet-name search box when navigating ArrowUp). -->\n <summary\n tabindex=\"-1\"\n [class.cursor-pointer]=\"collapsible()\"\n [class]=\"rowStateClass\"\n class=\"m-0 flex h-8 w-full scroll-mt-16 scroll-mb-3 items-center gap-1 pl-1 font-semibold select-none\"\n (click)=\"onHeaderClick($event)\">\n <ng-content select=\"label\">\n @let icon = aggregation()?.icon;\n @if (icon) {\n <FaIcon [faClass]=\"icon\" class=\"mr-1 shrink-0\" />\n }\n <span class=\"grow\">{{\n aggregation()?.display | syslang | transloco\n }}</span>\n </ng-content>\n\n @if (hasFilters()) {\n @let clearLabel = \"filters.clearFilters\" | transloco;\n <button\n type=\"button\"\n variant=\"tertiary\"\n [iconOnly]=\"true\"\n size=\"sm\"\n class=\"shrink-0\"\n [aria-label]=\"clearLabel\"\n [tooltip]=\"clearLabel\"\n (click)=\"$event.stopPropagation(); clear()\">\n <filter-x-icon class=\"shrink-0\" />\n </button>\n }\n\n @if (\n (!isControlled() || emitOn() === \"apply\") &&\n selection() &&\n validSelection()\n ) {\n @let label = \"filters.apply\" | transloco;\n <button\n type=\"button\"\n variant=\"accent\"\n size=\"sm\"\n class=\"overflow-hidden\"\n [aria-label]=\"label\"\n [tooltip]=\"label\"\n (click)=\"$event.stopPropagation(); apply()\">\n <FilterIcon class=\"shrink-0\" />\n <span class=\"truncate\">{{ label }}</span>\n </button>\n }\n @if (collapsible()) {\n <!-- The `<summary>` is out of the tab order (`tabindex=\"-1\"`), so this button is the keyboard way to\n collapse the facet. Its click bubbles to the `<summary>`, whose handler does the toggle, and it\n exposes `aria-expanded`. -->\n <button\n type=\"button\"\n variant=\"tertiary\"\n [iconOnly]=\"true\"\n size=\"sm\"\n [aria-label]=\"'filters.toggle' | transloco\"\n [attr.aria-expanded]=\"!isCollapsed()\"\n [tooltip]=\"'filters.toggle' | transloco\"\n class=\"shrink-0 cursor-pointer [&_svg]:transition-transform [&_svg]:duration-150 group-open:[&_svg]:rotate-90\">\n <chevron-right class=\"shrink-0\" />\n </button>\n }\n </summary>\n\n <!-- content wrapper -->\n <!-- RGAA 11.5 / WCAG 1.3.1: the radios below all share one `name`, so they are one set\n and have to be exposed as one. `role=\"group\"` here rather than `role=\"radiogroup\"`\n on the list: a radiogroup owns its radios directly, and the <li> in between would be\n invalid owned children — the list would also stop announcing how many options it\n holds. The name is the aggregation's own title, the words the header already shows;\n with no title the group stays unnamed rather than carrying an empty label. -->\n @let groupName = aggregation()?.display;\n <form (keydown.enter)=\"onOptionEnter($event)\">\n <div\n role=\"group\"\n [attr.aria-label]=\"groupName ? (groupName | syslang | transloco) : null\">\n <ul\n class=\"flex max-h-(--scroll-height,20rem) snap-y snap-start scrollbar-thin flex-col gap-0.5 overflow-auto pt-2\"\n role=\"list\">\n @for (option of dateOptions(); track $index) {\n <li\n role=\"listitem\"\n [attr.aria-label]=\"option.display | syslang | transloco\"\n [class]=\"\n cn(\n itemStateClass,\n 'flex cursor-pointer items-center p-2 text-sm text-(--font-neutral-base) select-none',\n dateModel().option === option.display &&\n 'bg-(--bg-primary-lighter)',\n option.hidden && 'hidden',\n option.disabled && 'pointer-events-none opacity-40'\n )\n \"\n [attr.aria-hidden]=\"option.disabled\">\n <radio\n name=\"date-filter\"\n class=\"grow\"\n [checked]=\"dateModel().option === option.display\"\n [disabled]=\"option.disabled\"\n (checkedChange)=\"selectOption(option.display)\">\n {{ option.display | syslang | transloco }}\n </radio>\n </li>\n }\n\n @if (allowCustomRange) {\n <li\n role=\"listitem\"\n aria-label=\"open date range picker\"\n [class]=\"\n cn(\n itemStateClass,\n 'flex cursor-pointer items-center gap-(--spacing-xs) p-2 text-sm text-(--font-neutral-base) select-none',\n dateModel().option === 'custom-range' &&\n 'bg-(--bg-primary-lighter)'\n )\n \">\n <radio\n name=\"date-filter\"\n class=\"grow items-center\"\n [checked]=\"dateModel().option === 'custom-range'\"\n (checkedChange)=\"selectOption('custom-range')\">\n <div class=\"flex flex-wrap items-center gap-x-1 gap-y-1\">\n <div class=\"flex items-center gap-1\">\n <label\n [attr.for]=\"startId\"\n class=\"radio-label min-w-8 truncate\"\n >{{ \"filters.from\" | transloco }}</label\n >\n <input-group\n class=\"h-8 max-w-[18ch] min-w-[18ch] has-[[readonly]]:border-(--stroke-primary-muted)\">\n <input\n input-control\n [id]=\"startId\"\n name=\"start\"\n type=\"text\"\n readonly\n [value]=\"customRangeFrom()\"\n (click)=\"selectAndOpenDialog()\"\n (keydown.enter)=\"onRangeInputEnter($event)\" />\n </input-group>\n </div>\n <div class=\"flex items-center gap-1\">\n <label\n [attr.for]=\"endId\"\n class=\"radio-label min-w-8 truncate text-right\"\n >{{ \"filters.to\" | transloco }}</label\n >\n <input-group\n class=\"h-8 max-w-[18ch] min-w-[18ch] has-[[readonly]]:border-(--stroke-primary-muted)\">\n <input\n input-control\n [id]=\"endId\"\n name=\"end\"\n type=\"text\"\n readonly\n [value]=\"customRangeTo()\"\n (click)=\"selectAndOpenDialog()\"\n (keydown.enter)=\"onRangeInputEnter($event)\" />\n </input-group>\n </div>\n </div>\n </radio>\n </li>\n }\n </ul>\n </div>\n </form>\n</details>\n",
23537
+ template: "<!-- `<details>`/`<summary>` stay native (open/close, the `name`-based exclusive accordion). A facet is a\n disclosure, not a grid row: see `aggregation-panel.html` for the why (same pattern, duplicated here\n since this facet type doesn't go through `AggregationPanelComponent`). The `<summary>` itself is\n `tabindex=\"-1\"`: the header's buttons (apply, clear, chevron) are the keyboard stops, see the\n chevron below. -->\n<details [attr.open]=\"expanded()\" [attr.name]=\"id()\" class=\"group space-y-2\">\n <!-- scroll-mb-3/scroll-mt-16: see aggregation-panel-row.html for the full rationale\n (bottom margin against the container's edge, much bigger top margin to also clear\n <More>'s sticky facet-name search box when navigating ArrowUp). -->\n <summary\n tabindex=\"-1\"\n [class.cursor-pointer]=\"collapsible()\"\n [class]=\"rowStateClass\"\n class=\"m-0 flex h-8 w-full scroll-mt-16 scroll-mb-3 items-center gap-1 pl-1 font-semibold select-none\"\n (click)=\"onHeaderClick($event)\">\n <ng-content select=\"label\">\n @let icon = aggregation()?.icon;\n @if (icon) {\n <FaIcon [faClass]=\"icon\" class=\"mr-1 shrink-0\" />\n }\n <span class=\"grow\">{{\n aggregation()?.display | syslang | transloco\n }}</span>\n </ng-content>\n\n @if (hasFilters()) {\n @let clearLabel = \"filters.clearFilters\" | transloco;\n <button\n type=\"button\"\n variant=\"tertiary\"\n [iconOnly]=\"true\"\n size=\"sm\"\n class=\"shrink-0\"\n [aria-label]=\"clearLabel\"\n [tooltip]=\"clearLabel\"\n (click)=\"$event.stopPropagation(); clear()\">\n <filter-x-icon class=\"shrink-0\" />\n </button>\n }\n\n @if (\n (!isControlled() || emitOn() === \"apply\") &&\n selection() &&\n validSelection()\n ) {\n @let label = \"filters.apply\" | transloco;\n <button\n type=\"button\"\n variant=\"accent\"\n size=\"sm\"\n class=\"overflow-hidden\"\n [aria-label]=\"label\"\n [tooltip]=\"label\"\n (click)=\"$event.stopPropagation(); apply()\">\n <FilterIcon class=\"shrink-0\" />\n <span class=\"truncate\">{{ label }}</span>\n </button>\n }\n @if (collapsible()) {\n <!-- The `<summary>` is out of the tab order (`tabindex=\"-1\"`), so this button is the keyboard way to\n collapse the facet. Its click bubbles to the `<summary>`, whose handler does the toggle, and it\n exposes `aria-expanded`. -->\n <button\n type=\"button\"\n variant=\"tertiary\"\n [iconOnly]=\"true\"\n size=\"sm\"\n [aria-label]=\"'filters.toggle' | transloco\"\n [attr.aria-expanded]=\"!isCollapsed()\"\n [tooltip]=\"'filters.toggle' | transloco\"\n class=\"shrink-0 cursor-pointer [&_svg]:transition-transform [&_svg]:duration-150 group-open:[&_svg]:rotate-90\">\n <chevron-right class=\"shrink-0\" />\n </button>\n }\n </summary>\n\n <!-- content wrapper -->\n <!-- RGAA 11.5 / WCAG 1.3.1: the radios below all share one `name`, so they are one set\n and have to be exposed as one. `role=\"group\"` here rather than `role=\"radiogroup\"`\n on the list: a radiogroup owns its radios directly, and the <li> in between would be\n invalid owned children — the list would also stop announcing how many options it\n holds. The name is the aggregation's own title, the words the header already shows;\n with no title the group stays unnamed rather than carrying an empty label. -->\n @let groupName = aggregation()?.display;\n <form (keydown.enter)=\"onOptionEnter($event)\">\n <div\n role=\"group\"\n [attr.aria-label]=\"groupName ? (groupName | syslang | transloco) : null\">\n <ul\n class=\"flex max-h-(--scroll-height,20rem) snap-y snap-start scrollbar-thin flex-col gap-0.5 overflow-auto pt-2\"\n role=\"list\">\n @for (option of dateOptions(); track option.display) {\n <li\n role=\"listitem\"\n [attr.aria-label]=\"\n maskedLabel(option) ?? (option.display | syslang | transloco)\n \"\n [class]=\"\n cn(\n itemStateClass,\n 'flex cursor-pointer items-center p-2 text-sm text-(--font-neutral-base) select-none',\n dateModel().option === option.display &&\n 'bg-(--bg-primary-lighter)',\n option.hidden && 'hidden',\n option.disabled && 'pointer-events-none opacity-40'\n )\n \"\n [attr.aria-hidden]=\"option.disabled\">\n <radio\n name=\"date-filter\"\n class=\"grow\"\n [checked]=\"dateModel().option === option.display\"\n [disabled]=\"option.disabled\"\n (checkedChange)=\"selectOption(option.display)\">\n {{\n maskedLabel(option) ?? (option.display | syslang | transloco)\n }}\n </radio>\n </li>\n }\n\n @if (allowCustomRange) {\n <li\n role=\"listitem\"\n aria-label=\"open date range picker\"\n [class]=\"\n cn(\n itemStateClass,\n 'flex cursor-pointer items-center gap-(--spacing-xs) p-2 text-sm text-(--font-neutral-base) select-none',\n dateModel().option === 'custom-range' &&\n 'bg-(--bg-primary-lighter)'\n )\n \">\n <radio\n name=\"date-filter\"\n class=\"grow items-center\"\n [checked]=\"dateModel().option === 'custom-range'\"\n (checkedChange)=\"selectOption('custom-range')\">\n <div class=\"flex flex-wrap items-center gap-x-1 gap-y-1\">\n <div class=\"flex items-center gap-1\">\n <label\n [attr.for]=\"startId\"\n class=\"radio-label min-w-8 truncate\"\n >{{ \"filters.from\" | transloco }}</label\n >\n <input-group\n class=\"h-8 max-w-[18ch] min-w-[18ch] has-[[readonly]]:border-(--stroke-primary-muted)\">\n <input\n input-control\n [id]=\"startId\"\n name=\"start\"\n type=\"text\"\n readonly\n [value]=\"customRangeFrom()\"\n (click)=\"selectAndOpenDialog()\"\n (keydown.enter)=\"onRangeInputEnter($event)\" />\n </input-group>\n </div>\n <div class=\"flex items-center gap-1\">\n <label\n [attr.for]=\"endId\"\n class=\"radio-label min-w-8 truncate text-right\"\n >{{ \"filters.to\" | transloco }}</label\n >\n <input-group\n class=\"h-8 max-w-[18ch] min-w-[18ch] has-[[readonly]]:border-(--stroke-primary-muted)\">\n <input\n input-control\n [id]=\"endId\"\n name=\"end\"\n type=\"text\"\n readonly\n [value]=\"customRangeTo()\"\n (click)=\"selectAndOpenDialog()\"\n (keydown.enter)=\"onRangeInputEnter($event)\" />\n </input-group>\n </div>\n </div>\n </radio>\n </li>\n }\n </ul>\n </div>\n </form>\n</details>\n",
23488
23538
  styles: [":host{min-width:200px}ul[role=list]{scrollbar-width:thin}\n"]
23489
23539
  }]
23490
23540
  }],
@@ -23622,6 +23672,10 @@ i0.ɵɵngDeclareClassMetadata({
23622
23672
  type: i0.Output,
23623
23673
  args: ["filtersChange"]
23624
23674
  }],
23675
+ aggregationOverridesChange: [{
23676
+ type: i0.Output,
23677
+ args: ["aggregationOverridesChange"]
23678
+ }],
23625
23679
  searchText: [{
23626
23680
  type: i0.Input,
23627
23681
  args: [{
@@ -23866,19 +23920,7 @@ var AggregationPanelComponent = class AggregationPanelComponent {
23866
23920
  {
23867
23921
  kind: "component",
23868
23922
  type: SearchInputComponent,
23869
- selector: "SearchInput, search-input, searchinput",
23870
- inputs: [
23871
- "value",
23872
- "placeholder",
23873
- "disabled",
23874
- "aria-label",
23875
- "hotkey",
23876
- "clearLabel",
23877
- "class",
23878
- "size",
23879
- "textSize"
23880
- ],
23881
- outputs: ["valueChange"]
23923
+ selector: "SearchInput:not([suggestions]), search-input:not([suggestions]), searchinput:not([suggestions])"
23882
23924
  },
23883
23925
  {
23884
23926
  kind: "component",
@@ -26474,6 +26516,7 @@ var AggregationComponent = class AggregationComponent {
26474
26516
  "onClear",
26475
26517
  "selectionChange",
26476
26518
  "filtersChange",
26519
+ "aggregationOverridesChange",
26477
26520
  "searchTextChange"
26478
26521
  ]
26479
26522
  }
@@ -27691,7 +27734,8 @@ var FilterButtonComponent = class FilterButtonComponent {
27691
27734
  "offset",
27692
27735
  "config",
27693
27736
  "lockPosition",
27694
- "matchTriggerWidth"
27737
+ "matchTriggerWidth",
27738
+ "hideWhenTriggerHidden"
27695
27739
  ],
27696
27740
  exportAs: ["popover", "dropdown"]
27697
27741
  },
@@ -28112,19 +28156,7 @@ var MoreComponent = class MoreComponent {
28112
28156
  {
28113
28157
  kind: "component",
28114
28158
  type: SearchInputComponent,
28115
- selector: "SearchInput, search-input, searchinput",
28116
- inputs: [
28117
- "value",
28118
- "placeholder",
28119
- "disabled",
28120
- "aria-label",
28121
- "hotkey",
28122
- "clearLabel",
28123
- "class",
28124
- "size",
28125
- "textSize"
28126
- ],
28127
- outputs: ["valueChange"]
28159
+ selector: "SearchInput:not([suggestions]), search-input:not([suggestions]), searchinput:not([suggestions])"
28128
28160
  },
28129
28161
  {
28130
28162
  kind: "pipe",
@@ -28538,7 +28570,8 @@ var MoreButtonComponent = class MoreButtonComponent {
28538
28570
  "offset",
28539
28571
  "config",
28540
28572
  "lockPosition",
28541
- "matchTriggerWidth"
28573
+ "matchTriggerWidth",
28574
+ "hideWhenTriggerHidden"
28542
28575
  ],
28543
28576
  exportAs: ["popover", "dropdown"]
28544
28577
  },