@sinequa/atomic-angular 2.10.0 → 2.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,7 +2,7 @@ import * as i0 from "@angular/core";
2
2
  import { ChangeDetectionStrategy, ChangeDetectorRef, Component, DOCUMENT, DestroyRef, Directive, ElementRef, EnvironmentInjector, HostBinding, HostListener, Inject, Injectable, InjectionToken, Injector, LOCALE_ID, Optional, Pipe, Service, TemplateRef, ViewEncapsulation, afterNextRender, afterRenderEffect, assertInInjectionContext, booleanAttribute, computed, contentChild, contentChildren, effect, inject, input, isDevMode, linkedSignal, makeEnvironmentProviders, model, numberAttribute, output, provideEnvironmentInitializer, resource, runInInjectionContext, signal, untracked, viewChild, viewChildren } from "@angular/core";
3
3
  import { BehaviorSubject, EMPTY, Subject, Subscription, catchError, debounceTime, filter, firstValueFrom, from, fromEvent, map, of, scan, shareReplay, switchMap, tap, throwError } from "rxjs";
4
4
  import { TranslocoPipe, TranslocoService, provideTranslocoScope } from "@jsverse/transloco";
5
- import { ArrowDownIcon, ArrowDownZaIcon, ArrowLeftIcon, ArrowRightFromBracketIcon, ArrowRightIcon, ArrowUpAzIcon, ArrowUpIcon, ArrowUpRightFromSquareIcon, ArrowsMaximizeIcon, AutocompleteComponent, AvatarComponent, AvatarFallbackComponent, AvatarImageComponent, BadgeComponent, BarsIcon, BellIcon, BellSlashIcon, BookmarkIcon, BreakpointObserverService, BuildingIcon, ButtonComponent, CalendarDayIcon, CheckIcon, CheckboxComponent, ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon, ChevronsLeftIcon, ChevronsRightIcon, CircleCheckIcon, ClockIcon, CommentIcon, CommentsIcon, DIALOG_REF, DesktopIcon, DialogBodyComponent, DialogComponent, DialogContentComponent, DialogDescriptionComponent, DialogFooterComponent, DialogHeaderComponent, DialogTitleComponent, DropdownDirective, EditIcon, EllipsisIcon, EllipsisVerticalIcon, EyeIcon, EyeSlashIcon, FileHTMLIcon, FileIcon, FileOutputIcon, FileSpreadsheetIcon, FileTextIcon, FlagEnglishIcon, FlagFrenchIcon, FlagGermanIcon, FlashlightIcon, FolderIcon, FolderOpenIcon, FrownIcon, GearIcon, GitForkIcon, GlobeIcon, GripVerticalIcon, HighlighterIcon, HistoryIcon, HomeIcon, ImageIcon, InboxIcon, InfoCircleIcon, InputComponent, InputControlDirective, InputIconRightDirective, KbdComponent, KbdGroupComponent, KeyIcon, KeyboardIcon, LanguageIcon, LightbulbIcon, LightbulbSlashIcon, LinkComponent, ListComponent, ListItemActionComponent, ListItemAvatarDirective, ListItemComponent, ListItemIconDirective, ListItemLeftDirective, ListItemTextComponent, LoadingCircleIcon, LockIcon, MagnifyingGlassIcon, MagnifyingGlassMinusIcon, MagnifyingGlassPlusIcon, MapPinIcon, MenuComponent, MenuContentComponent, MenuItemComponent, MenuTriggerDirective, MoonIcon, PaletteIcon, PaperclipIcon, PenToSquareIcon, PlusIcon, PopoverDirective, PopoverResizeHandleComponent, PresentationIcon, QuestionCircleIcon, QuickFilterIcon, RadioComponent, RobotIcon, SaveIcon, SearchBarComponent, SearchBarSuggestionsComponent, SearchIcon, SearchInputComponent, SelectComponent, SelectTriggerDirective, SendIcon, SheetBodyComponent, SheetComponent, SheetHeaderComponent, SidebarInsetDirective, SlashIcon, SlidersIcon, SortableDirective, SortableItemDirective, SparklesIcon, SpinnerIcon, SquareCheckIcon, SquareIcon, SquareMinusIcon, StarIcon, SunBrightIcon, SwitchComponent, SyncIcon, TabComponent, TabListComponent, TabPanelComponent, TabsComponent, TagComponent, TagIcon, TagInputComponent, TagsIcon, TextareaComponent, TextareaControlDirective, ThumbsDownIcon, ThumbsUpIcon, ToggleComponent, ToggleGroupComponent, ToggleItemComponent, ToggleLeftIcon, ToggleRightIcon, TooltipDirective, TrashCanIcon, TrashIcon, TreeComponent, TreeItemComponent, TreeItemExpanderComponent, TreeItemGroupComponent, TriangleAlertIcon, TruncatedDirective, UndoIcon, UserIcon, UserSecretIcon, VideoIcon, XMarkIcon, createCallable, injectCallRef, injectFloatingPanel, isApplePlatform } from "@sinequa/galactik";
5
+ import { ArrowDownIcon, ArrowDownZaIcon, ArrowLeftIcon, ArrowRightFromBracketIcon, ArrowRightIcon, ArrowUpAzIcon, ArrowUpIcon, ArrowUpRightFromSquareIcon, ArrowsMaximizeIcon, AutocompleteComponent, AvatarComponent, AvatarFallbackComponent, AvatarImageComponent, BadgeComponent, BarsIcon, BellIcon, BellSlashIcon, BookmarkIcon, BreakpointObserverService, BuildingIcon, ButtonComponent, CalendarDayIcon, CheckIcon, CheckboxComponent, ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon, ChevronsLeftIcon, ChevronsRightIcon, CircleCheckIcon, ClockIcon, CommentIcon, CommentsIcon, DIALOG_REF, DesktopIcon, DialogBodyComponent, DialogComponent, DialogContentComponent, DialogDescriptionComponent, DialogFooterComponent, DialogHeaderComponent, DialogTitleComponent, DropdownDirective, EditIcon, EllipsisIcon, EllipsisVerticalIcon, EyeIcon, EyeSlashIcon, FileHTMLIcon, FileIcon, FileOutputIcon, FileSpreadsheetIcon, FileTextIcon, FilterIcon, FlagEnglishIcon, FlagFrenchIcon, FlagGermanIcon, FlashlightIcon, FolderIcon, FolderOpenIcon, FrownIcon, GearIcon, GitForkIcon, GlobeIcon, GripVerticalIcon, HighlighterIcon, HistoryIcon, HomeIcon, ImageIcon, InboxIcon, InfoCircleIcon, InputComponent, InputControlDirective, InputIconRightDirective, KbdComponent, KbdGroupComponent, KeyIcon, KeyboardIcon, LanguageIcon, LightbulbIcon, LightbulbSlashIcon, LinkComponent, ListComponent, ListItemActionComponent, ListItemAvatarDirective, ListItemComponent, ListItemIconDirective, ListItemLeftDirective, ListItemTextComponent, LoadingCircleIcon, LockIcon, MagnifyingGlassIcon, MagnifyingGlassMinusIcon, MagnifyingGlassPlusIcon, MapPinIcon, MenuComponent, MenuContentComponent, MenuItemComponent, MenuTriggerDirective, MoonIcon, PaletteIcon, PaperclipIcon, PenToSquareIcon, PlusIcon, PopoverDirective, PopoverResizeHandleComponent, PresentationIcon, QuestionCircleIcon, QuickFilterIcon, RadioComponent, RobotIcon, SaveIcon, SearchBarComponent, SearchBarSuggestionsComponent, SearchIcon, SearchInputComponent, SelectComponent, SelectTriggerDirective, SendIcon, SheetBodyComponent, SheetComponent, SheetHeaderComponent, SidebarInsetDirective, SlashIcon, SlidersIcon, SortableDirective, SortableItemDirective, SparklesIcon, SpinnerIcon, SquareCheckIcon, SquareIcon, SquareMinusIcon, StarIcon, SunBrightIcon, SwitchComponent, SyncIcon, TabComponent, TabListComponent, TabPanelComponent, TabsComponent, TagComponent, TagIcon, TagInputComponent, TagsIcon, TextareaComponent, TextareaControlDirective, ThumbsDownIcon, ThumbsUpIcon, ToggleComponent, ToggleGroupComponent, ToggleItemComponent, ToggleLeftIcon, ToggleRightIcon, TooltipDirective, TrashCanIcon, TrashIcon, TreeComponent, TreeItemComponent, TreeItemExpanderComponent, TreeItemGroupComponent, TriangleAlertIcon, TruncatedDirective, UndoIcon, UserIcon, UserSecretIcon, VideoIcon, XMarkIcon, createCallable, injectCallRef, injectFloatingPanel, isApplePlatform } from "@sinequa/galactik";
6
6
  import { withDevtools } from "@ngrx-toolkit/core";
7
7
  import { getState, patchState, signalStore, signalStoreFeature, type, withComputed, withMethods, withState } from "@ngrx/signals";
8
8
  import { ActivatedRoute, NavigationEnd, Router, RouterLink, RouterModule } from "@angular/router";
@@ -5521,6 +5521,39 @@ i0.ɵɵngDeclareClassMetadata({
5521
5521
  args: [{ name: "fileSize" }]
5522
5522
  }]
5523
5523
  });
5524
+ var LocalizedNumberPipe = class LocalizedNumberPipe {
5525
+ transloco = inject(TranslocoService);
5526
+ lang = toSignal(this.transloco.langChanges$, { initialValue: this.transloco.getActiveLang() });
5527
+ transform(value) {
5528
+ return new Intl.NumberFormat(this.lang()).format(value ?? 0);
5529
+ }
5530
+ static ɵfac = i0.ɵɵngDeclareFactory({
5531
+ minVersion: "12.0.0",
5532
+ version: "22.2.1",
5533
+ ngImport: i0,
5534
+ type: LocalizedNumberPipe,
5535
+ deps: [],
5536
+ target: i0.ɵɵFactoryTarget.Pipe
5537
+ });
5538
+ static ɵpipe = i0.ɵɵngDeclarePipe({
5539
+ minVersion: "14.0.0",
5540
+ version: "22.2.1",
5541
+ ngImport: i0,
5542
+ type: LocalizedNumberPipe,
5543
+ isStandalone: true,
5544
+ name: "localizedNumber"
5545
+ });
5546
+ };
5547
+ i0.ɵɵngDeclareClassMetadata({
5548
+ minVersion: "12.0.0",
5549
+ version: "22.2.1",
5550
+ ngImport: i0,
5551
+ type: LocalizedNumberPipe,
5552
+ decorators: [{
5553
+ type: Pipe,
5554
+ args: [{ name: "localizedNumber" }]
5555
+ }]
5556
+ });
5524
5557
  var MetadataComponent = class MetadataComponent {
5525
5558
  click = output();
5526
5559
  class = input(...ngDevMode ? [void 0, { debugName: "class" }] : /* istanbul ignore next */ []);
@@ -8218,6 +8251,8 @@ var NavbarTabsComponent = class NavbarTabsComponent {
8218
8251
  appStore = inject(AppStore);
8219
8252
  class = input(...ngDevMode ? [void 0, { debugName: "class" }] : /* istanbul ignore next */ []);
8220
8253
  ariaLabel = input(...ngDevMode ? [void 0, { debugName: "ariaLabel" }] : /* istanbul ignore next */ []);
8254
+ variant = input("primary", ...ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []);
8255
+ isPill = computed(() => this.variant() === "primary", ...ngDevMode ? [{ debugName: "isPill" }] : /* istanbul ignore next */ []);
8221
8256
  showCount = input(true, {
8222
8257
  ...ngDevMode ? { debugName: "showCount" } : /* istanbul ignore next */ {},
8223
8258
  transform: booleanAttribute
@@ -8325,6 +8360,13 @@ var NavbarTabsComponent = class NavbarTabsComponent {
8325
8360
  isRequired: false,
8326
8361
  transformFunction: null
8327
8362
  },
8363
+ variant: {
8364
+ classPropertyName: "variant",
8365
+ publicName: "variant",
8366
+ isSignal: true,
8367
+ isRequired: false,
8368
+ transformFunction: null
8369
+ },
8328
8370
  showCount: {
8329
8371
  classPropertyName: "showCount",
8330
8372
  publicName: "showCount",
@@ -8382,10 +8424,11 @@ var NavbarTabsComponent = class NavbarTabsComponent {
8382
8424
  WHICH list it landed in — and a page may hold more than one. The default is translated
8383
8425
  so every consumer gets a named tab list without passing anything.
8384
8426
  (No backticks in this comment: the template is a template literal.) -->
8385
- <TabList class="w-full" [attr.aria-label]="ariaLabel() ?? ('navbarTabs.tabs' | transloco)" [(selectedTab)]="selectedTab" (selectedTabChange)="onSelectTab($event)" selectionMode="explicit" [softDisabled]="false">
8427
+ <TabList class="w-full" [variant]="variant()" [attr.aria-label]="ariaLabel() ?? ('navbarTabs.tabs' | transloco)" [(selectedTab)]="selectedTab" (selectedTabChange)="onSelectTab($event)" selectionMode="explicit" [softDisabled]="false">
8386
8428
  @for (tab of nav.tabs(); track $index) {
8387
8429
  <Tab
8388
- [class]="cn('group', !noTruncate() && 'basis-50')"
8430
+ [variant]="variant()"
8431
+ [class]="cn('group', !noTruncate() && 'basis-50', !isPill() && 'h-11 px-(--spacing-xs) text-(length:--size-base) font-semibold')"
8389
8432
  [value]="tab.path"
8390
8433
  overflowItem
8391
8434
  [noTruncate]="noTruncate()"
@@ -8416,315 +8459,11 @@ var NavbarTabsComponent = class NavbarTabsComponent {
8416
8459
  </div>
8417
8460
  </Tab>
8418
8461
  }
8419
- <!-- The overflow trigger belongs INSIDE the tab list, and it is a tab: a tablist may only
8420
- contain tab elements, so a plain button here would break the announced tab count and
8421
- the arrow-key navigation. ngTab gives it the role and enrols it in the roving tabindex
8422
- — so the list keeps ONE tab stop and the arrows reach it like any other tab.
8423
-
8424
- It stays a real <button> because that is what menuTrigger needs: it wires the menu
8425
- through the native popovertarget attribute, which only works on a button.
8426
-
8427
- ms-auto pins it to the right end of the flex tab list. overflowStop is found by content
8428
- query, so the measurement is unaffected by where it sits. Always mounted so the stop
8429
- position is stable; hidden when every tab fits.
8430
- (No backticks in this comment: the template is a template literal.) -->
8431
- <button
8432
- ngTab
8433
- [value]="MORE_TABS_VALUE"
8434
- [menuTrigger]="moreTabsMenu"
8435
- variant="tertiary" [iconOnly]="true"
8436
- [attr.aria-label]="'navbarTabs.moreTabs' | transloco" [tooltip]="'navbarTabs.moreTabs' | transloco" overflowStop
8437
- [disabled]="moreTabs().length === 0"
8438
- class="ms-auto shrink-0"
8439
- [class.invisible]="moreTabs().length === 0">
8440
- <EllipsisIcon orientation="vertical"></EllipsisIcon>
8441
- <span class="sr-only">{{ "navbarTabs.moreTabs" | transloco }}</span>
8442
- </button>
8443
- </TabList>
8444
- </Tabs>
8462
+ <!-- Pill strip: ms-auto pins the trigger to the right end of the strip. Underline variants: no pin, the trigger
8463
+ follows the last visible tab, which is where the eye expects the rest of the list to be. The overflow
8464
+ manager reads only its size, never its position, so both placements measure the same.
8445
8465
 
8446
- <Menu #moreTabsMenu="menu" placement="bottom-end">
8447
- <MenuContent class="z-500 w-fit">
8448
- @for (tab of moreTabs(); track $index) {
8449
- <MenuItem [value]="tab" [disabled]="isTabDisabled(tab)">
8450
- <a class="inline-flex items-center gap-1 whitespace-nowrap first-letter:capitalize"
8451
- [routerLink]="isTabDisabled(tab) ? null : [tab.routerLink]"
8452
- [queryParams]="getQueryParams(tab)"
8453
- [queryParamsHandling]="getQueryParamsHandling()"
8454
- [attr.aria-selected]="nav.currentPath() === tab.path"
8455
- [attr.aria-label]="tab.display | syslang | transloco"
8456
- [attr.aria-disabled]="isTabDisabled(tab) ? true : null"
8457
- [attr.tabindex]="isTabDisabled(tab) ? -1 : null"
8458
- (click)="onOverflowItemClick($event, tab)"
8459
- >
8460
- @if (tab.icon) {
8461
- <FaIcon [faClass]="tab.icon"/>
8462
- }
8463
- {{ tab.display | syslang | transloco }}
8464
- </a>
8465
- </MenuItem>
8466
- }
8467
- </MenuContent>
8468
- </Menu>
8469
- </div>
8470
- }
8471
- `,
8472
- isInline: true,
8473
- dependencies: [
8474
- {
8475
- kind: "directive",
8476
- type: RouterLink,
8477
- selector: "[routerLink]",
8478
- inputs: [
8479
- "target",
8480
- "queryParams",
8481
- "fragment",
8482
- "queryParamsHandling",
8483
- "state",
8484
- "info",
8485
- "relativeTo",
8486
- "preserveFragment",
8487
- "skipLocationChange",
8488
- "replaceUrl",
8489
- "browserUrl",
8490
- "routerLink"
8491
- ]
8492
- },
8493
- {
8494
- kind: "directive",
8495
- type: ButtonComponent,
8496
- selector: "button",
8497
- inputs: [
8498
- "class",
8499
- "variant",
8500
- "scheme",
8501
- "iconOnly",
8502
- "size"
8503
- ]
8504
- },
8505
- {
8506
- kind: "directive",
8507
- type: TabsComponent,
8508
- selector: "Tabs",
8509
- inputs: ["class"]
8510
- },
8511
- {
8512
- kind: "directive",
8513
- type: TabListComponent,
8514
- selector: "TabList",
8515
- inputs: ["class", "variant"]
8516
- },
8517
- {
8518
- kind: "directive",
8519
- type: TabComponent,
8520
- selector: "Tab",
8521
- inputs: [
8522
- "class",
8523
- "variant",
8524
- "size",
8525
- "noTruncate"
8526
- ]
8527
- },
8528
- {
8529
- kind: "directive",
8530
- type: Tab,
8531
- selector: "[ngTab]",
8532
- inputs: [
8533
- "id",
8534
- "disabled",
8535
- "value"
8536
- ],
8537
- exportAs: ["ngTab"]
8538
- },
8539
- {
8540
- kind: "directive",
8541
- type: OverflowManagerDirective,
8542
- selector: "[overflowManager]",
8543
- inputs: [
8544
- "target",
8545
- "margin",
8546
- "direction",
8547
- "reserveStop"
8548
- ],
8549
- outputs: ["count"]
8550
- },
8551
- {
8552
- kind: "directive",
8553
- type: OverflowItemDirective,
8554
- selector: "[overflowItem]"
8555
- },
8556
- {
8557
- kind: "directive",
8558
- type: OverflowStopDirective,
8559
- selector: "[overflowStop]"
8560
- },
8561
- {
8562
- kind: "directive",
8563
- type: BadgeComponent,
8564
- selector: "badge, Badge",
8565
- inputs: [
8566
- "class",
8567
- "variant",
8568
- "fill",
8569
- "scheme",
8570
- "size",
8571
- "aria-label"
8572
- ]
8573
- },
8574
- {
8575
- kind: "component",
8576
- type: EllipsisIcon,
8577
- selector: "ellipsis-icon, EllipsisIcon, ellipsisicon",
8578
- inputs: ["class", "orientation"]
8579
- },
8580
- {
8581
- kind: "directive",
8582
- type: TooltipDirective,
8583
- selector: "[tooltip]",
8584
- inputs: [
8585
- "tooltip",
8586
- "tooltipPosition",
8587
- "tooltipEvent",
8588
- "showDelay",
8589
- "hideDelay",
8590
- "life",
8591
- "tooltipOffset",
8592
- "autoHide",
8593
- "hideOnEscape",
8594
- "escape",
8595
- "tooltipDisabled",
8596
- "tooltipArrow",
8597
- "tooltipStyleClass",
8598
- "tooltipOptions",
8599
- "tooltip-position",
8600
- "tooltip-delay",
8601
- "tooltip-duration",
8602
- "tooltip-offset"
8603
- ],
8604
- exportAs: ["tooltip"]
8605
- },
8606
- {
8607
- kind: "component",
8608
- type: FaIconComponent,
8609
- selector: "fa-icon, FaIcon",
8610
- inputs: ["faClass", "class"]
8611
- },
8612
- {
8613
- kind: "directive",
8614
- type: MenuTriggerDirective,
8615
- selector: "[menuTrigger]",
8616
- inputs: ["menuTrigger"],
8617
- exportAs: ["menuTrigger"]
8618
- },
8619
- {
8620
- kind: "directive",
8621
- type: MenuComponent,
8622
- selector: "Menu,menu",
8623
- inputs: ["class"],
8624
- exportAs: ["menu"]
8625
- },
8626
- {
8627
- kind: "component",
8628
- type: MenuContentComponent,
8629
- selector: "menu-content,MenuContent,menucontent",
8630
- inputs: ["class"]
8631
- },
8632
- {
8633
- kind: "directive",
8634
- type: MenuItemComponent,
8635
- selector: "menu-item,MenuItem,menuitem",
8636
- inputs: ["class", "checked"],
8637
- exportAs: ["menuitem"]
8638
- },
8639
- {
8640
- kind: "pipe",
8641
- type: TranslocoPipe,
8642
- name: "transloco"
8643
- },
8644
- {
8645
- kind: "pipe",
8646
- type: SyslangPipe,
8647
- name: "syslang"
8648
- }
8649
- ]
8650
- });
8651
- };
8652
- i0.ɵɵngDeclareClassMetadata({
8653
- minVersion: "12.0.0",
8654
- version: "22.2.1",
8655
- ngImport: i0,
8656
- type: NavbarTabsComponent,
8657
- decorators: [{
8658
- type: Component,
8659
- args: [{
8660
- selector: "navbar-tabs",
8661
- imports: [
8662
- RouterLink,
8663
- TranslocoPipe,
8664
- SyslangPipe,
8665
- ButtonComponent,
8666
- TabsComponent,
8667
- TabListComponent,
8668
- TabComponent,
8669
- Tab,
8670
- OverflowManagerDirective,
8671
- OverflowItemDirective,
8672
- OverflowStopDirective,
8673
- BadgeComponent,
8674
- EllipsisIcon,
8675
- TooltipDirective,
8676
- FaIconComponent,
8677
- MenuTriggerDirective,
8678
- MenuComponent,
8679
- MenuContentComponent,
8680
- MenuItemComponent
8681
- ],
8682
- providers: [provideTranslocoScope("navbar-tabs")],
8683
- template: `
8684
- <!-- do not display the tabs if there are no tabs -->
8685
- @if (nav.tabs().length > 0) {
8686
- <div overflowManager (count)="visibleTabCount.set($event)" class="flex items-end rounded-[inherit] bg-inherit">
8687
- <Tabs class="flex-1 min-w-0 overflow-hidden">
8688
- <!-- The tab list needs a name of its own: the tablist role announces itself and its item
8689
- count, but without an accessible name a screen reader has nothing to tell the user
8690
- WHICH list it landed in — and a page may hold more than one. The default is translated
8691
- so every consumer gets a named tab list without passing anything.
8692
- (No backticks in this comment: the template is a template literal.) -->
8693
- <TabList class="w-full" [attr.aria-label]="ariaLabel() ?? ('navbarTabs.tabs' | transloco)" [(selectedTab)]="selectedTab" (selectedTabChange)="onSelectTab($event)" selectionMode="explicit" [softDisabled]="false">
8694
- @for (tab of nav.tabs(); track $index) {
8695
- <Tab
8696
- [class]="cn('group', !noTruncate() && 'basis-50')"
8697
- [value]="tab.path"
8698
- overflowItem
8699
- [noTruncate]="noTruncate()"
8700
- [style.--tab-min-width]="tabMinWidth(!!tab.icon)"
8701
- [disabled]="isTabDisabled(tab) || !isTabVisible($index)"
8702
- >
8703
- <div [class]="cn('flex items-center content-start w-full gap-1', !noTruncate() && '@container overflow-hidden min-w-0')">
8704
- @if (tab.icon) {
8705
- <!-- the icon never shrinks: the label truncates first -->
8706
- <FaIcon [faClass]="tab.icon" class="shrink-0" aria-hidden="true" />
8707
- }
8708
- <!-- when the tab gets too narrow to show anything meaningful next to
8709
- the icon, hide the label entirely instead of a clipped sliver.
8710
- w-0 + invisible (not display: none) keeps the label's line box,
8711
- so the tab height doesn't collapse to the icon's height -->
8712
- <span [class]="cn(!noTruncate() && 'truncate', !noTruncate() && tab.icon && '@max-[3rem]:w-0 @max-[3rem]:invisible')">{{ tab.display | syslang | transloco }}</span>
8713
- <!-- Show count badge only if count is > 0 -->
8714
- @if((tab.count ?? 0) > 0) {
8715
- <Badge
8716
- variant="number"
8717
- fill="secondary"
8718
- scheme="sage"
8719
- size="xs"
8720
- [aria-label]="tab.count + ' ' + (tab.count === 1 ? ('navbarTabs.result' | transloco) : ('navbarTabs.results' | transloco))">
8721
- {{ tab.count }}
8722
- </Badge>
8723
- }
8724
- </div>
8725
- </Tab>
8726
- }
8727
- <!-- The overflow trigger belongs INSIDE the tab list, and it is a tab: a tablist may only
8466
+ The overflow trigger belongs INSIDE the tab list, and it is a tab: a tablist may only
8728
8467
  contain tab elements, so a plain button here would break the announced tab count and
8729
8468
  the arrow-key navigation. ngTab gives it the role and enrols it in the roving tabindex
8730
8469
  — so the list keeps ONE tab stop and the arrows reach it like any other tab.
@@ -8743,9 +8482,322 @@ i0.ɵɵngDeclareClassMetadata({
8743
8482
  variant="tertiary" [iconOnly]="true"
8744
8483
  [attr.aria-label]="'navbarTabs.moreTabs' | transloco" [tooltip]="'navbarTabs.moreTabs' | transloco" overflowStop
8745
8484
  [disabled]="moreTabs().length === 0"
8746
- class="ms-auto shrink-0"
8485
+ [class]="cn('shrink-0', isPill() ? 'ms-auto' : 'self-center')"
8747
8486
  [class.invisible]="moreTabs().length === 0">
8748
- <EllipsisIcon orientation="vertical"></EllipsisIcon>
8487
+ <EllipsisIcon [orientation]="isPill() ? 'vertical' : 'horizontal'"></EllipsisIcon>
8488
+ <span class="sr-only">{{ "navbarTabs.moreTabs" | transloco }}</span>
8489
+ </button>
8490
+ </TabList>
8491
+ </Tabs>
8492
+
8493
+ <Menu #moreTabsMenu="menu" placement="bottom-end">
8494
+ <MenuContent class="z-500 w-fit">
8495
+ @for (tab of moreTabs(); track $index) {
8496
+ <MenuItem [value]="tab" [disabled]="isTabDisabled(tab)">
8497
+ <a class="inline-flex items-center gap-1 whitespace-nowrap first-letter:capitalize"
8498
+ [routerLink]="isTabDisabled(tab) ? null : [tab.routerLink]"
8499
+ [queryParams]="getQueryParams(tab)"
8500
+ [queryParamsHandling]="getQueryParamsHandling()"
8501
+ [attr.aria-selected]="nav.currentPath() === tab.path"
8502
+ [attr.aria-label]="tab.display | syslang | transloco"
8503
+ [attr.aria-disabled]="isTabDisabled(tab) ? true : null"
8504
+ [attr.tabindex]="isTabDisabled(tab) ? -1 : null"
8505
+ (click)="onOverflowItemClick($event, tab)"
8506
+ >
8507
+ @if (tab.icon) {
8508
+ <FaIcon [faClass]="tab.icon"/>
8509
+ }
8510
+ {{ tab.display | syslang | transloco }}
8511
+ </a>
8512
+ </MenuItem>
8513
+ }
8514
+ </MenuContent>
8515
+ </Menu>
8516
+ </div>
8517
+ }
8518
+ `,
8519
+ isInline: true,
8520
+ dependencies: [
8521
+ {
8522
+ kind: "directive",
8523
+ type: RouterLink,
8524
+ selector: "[routerLink]",
8525
+ inputs: [
8526
+ "target",
8527
+ "queryParams",
8528
+ "fragment",
8529
+ "queryParamsHandling",
8530
+ "state",
8531
+ "info",
8532
+ "relativeTo",
8533
+ "preserveFragment",
8534
+ "skipLocationChange",
8535
+ "replaceUrl",
8536
+ "browserUrl",
8537
+ "routerLink"
8538
+ ]
8539
+ },
8540
+ {
8541
+ kind: "directive",
8542
+ type: ButtonComponent,
8543
+ selector: "button",
8544
+ inputs: [
8545
+ "class",
8546
+ "variant",
8547
+ "scheme",
8548
+ "iconOnly",
8549
+ "size"
8550
+ ]
8551
+ },
8552
+ {
8553
+ kind: "directive",
8554
+ type: TabsComponent,
8555
+ selector: "Tabs",
8556
+ inputs: ["class"]
8557
+ },
8558
+ {
8559
+ kind: "directive",
8560
+ type: TabListComponent,
8561
+ selector: "TabList",
8562
+ inputs: ["class", "variant"]
8563
+ },
8564
+ {
8565
+ kind: "directive",
8566
+ type: TabComponent,
8567
+ selector: "Tab",
8568
+ inputs: [
8569
+ "class",
8570
+ "variant",
8571
+ "size",
8572
+ "noTruncate"
8573
+ ]
8574
+ },
8575
+ {
8576
+ kind: "directive",
8577
+ type: Tab,
8578
+ selector: "[ngTab]",
8579
+ inputs: [
8580
+ "id",
8581
+ "disabled",
8582
+ "value"
8583
+ ],
8584
+ exportAs: ["ngTab"]
8585
+ },
8586
+ {
8587
+ kind: "directive",
8588
+ type: OverflowManagerDirective,
8589
+ selector: "[overflowManager]",
8590
+ inputs: [
8591
+ "target",
8592
+ "margin",
8593
+ "direction",
8594
+ "reserveStop"
8595
+ ],
8596
+ outputs: ["count"]
8597
+ },
8598
+ {
8599
+ kind: "directive",
8600
+ type: OverflowItemDirective,
8601
+ selector: "[overflowItem]"
8602
+ },
8603
+ {
8604
+ kind: "directive",
8605
+ type: OverflowStopDirective,
8606
+ selector: "[overflowStop]"
8607
+ },
8608
+ {
8609
+ kind: "directive",
8610
+ type: BadgeComponent,
8611
+ selector: "badge, Badge",
8612
+ inputs: [
8613
+ "class",
8614
+ "variant",
8615
+ "fill",
8616
+ "scheme",
8617
+ "size",
8618
+ "aria-label"
8619
+ ]
8620
+ },
8621
+ {
8622
+ kind: "component",
8623
+ type: EllipsisIcon,
8624
+ selector: "ellipsis-icon, EllipsisIcon, ellipsisicon",
8625
+ inputs: ["class", "orientation"]
8626
+ },
8627
+ {
8628
+ kind: "directive",
8629
+ type: TooltipDirective,
8630
+ selector: "[tooltip]",
8631
+ inputs: [
8632
+ "tooltip",
8633
+ "tooltipPosition",
8634
+ "tooltipEvent",
8635
+ "showDelay",
8636
+ "hideDelay",
8637
+ "life",
8638
+ "tooltipOffset",
8639
+ "autoHide",
8640
+ "hideOnEscape",
8641
+ "escape",
8642
+ "tooltipDisabled",
8643
+ "tooltipArrow",
8644
+ "tooltipStyleClass",
8645
+ "tooltipOptions",
8646
+ "tooltip-position",
8647
+ "tooltip-delay",
8648
+ "tooltip-duration",
8649
+ "tooltip-offset"
8650
+ ],
8651
+ exportAs: ["tooltip"]
8652
+ },
8653
+ {
8654
+ kind: "component",
8655
+ type: FaIconComponent,
8656
+ selector: "fa-icon, FaIcon",
8657
+ inputs: ["faClass", "class"]
8658
+ },
8659
+ {
8660
+ kind: "directive",
8661
+ type: MenuTriggerDirective,
8662
+ selector: "[menuTrigger]",
8663
+ inputs: ["menuTrigger"],
8664
+ exportAs: ["menuTrigger"]
8665
+ },
8666
+ {
8667
+ kind: "directive",
8668
+ type: MenuComponent,
8669
+ selector: "Menu,menu",
8670
+ inputs: ["class"],
8671
+ exportAs: ["menu"]
8672
+ },
8673
+ {
8674
+ kind: "component",
8675
+ type: MenuContentComponent,
8676
+ selector: "menu-content,MenuContent,menucontent",
8677
+ inputs: ["class"]
8678
+ },
8679
+ {
8680
+ kind: "directive",
8681
+ type: MenuItemComponent,
8682
+ selector: "menu-item,MenuItem,menuitem",
8683
+ inputs: ["class", "checked"],
8684
+ exportAs: ["menuitem"]
8685
+ },
8686
+ {
8687
+ kind: "pipe",
8688
+ type: TranslocoPipe,
8689
+ name: "transloco"
8690
+ },
8691
+ {
8692
+ kind: "pipe",
8693
+ type: SyslangPipe,
8694
+ name: "syslang"
8695
+ }
8696
+ ]
8697
+ });
8698
+ };
8699
+ i0.ɵɵngDeclareClassMetadata({
8700
+ minVersion: "12.0.0",
8701
+ version: "22.2.1",
8702
+ ngImport: i0,
8703
+ type: NavbarTabsComponent,
8704
+ decorators: [{
8705
+ type: Component,
8706
+ args: [{
8707
+ selector: "navbar-tabs",
8708
+ imports: [
8709
+ RouterLink,
8710
+ TranslocoPipe,
8711
+ SyslangPipe,
8712
+ ButtonComponent,
8713
+ TabsComponent,
8714
+ TabListComponent,
8715
+ TabComponent,
8716
+ Tab,
8717
+ OverflowManagerDirective,
8718
+ OverflowItemDirective,
8719
+ OverflowStopDirective,
8720
+ BadgeComponent,
8721
+ EllipsisIcon,
8722
+ TooltipDirective,
8723
+ FaIconComponent,
8724
+ MenuTriggerDirective,
8725
+ MenuComponent,
8726
+ MenuContentComponent,
8727
+ MenuItemComponent
8728
+ ],
8729
+ providers: [provideTranslocoScope("navbar-tabs")],
8730
+ template: `
8731
+ <!-- do not display the tabs if there are no tabs -->
8732
+ @if (nav.tabs().length > 0) {
8733
+ <div overflowManager (count)="visibleTabCount.set($event)" class="flex items-end rounded-[inherit] bg-inherit">
8734
+ <Tabs class="flex-1 min-w-0 overflow-hidden">
8735
+ <!-- The tab list needs a name of its own: the tablist role announces itself and its item
8736
+ count, but without an accessible name a screen reader has nothing to tell the user
8737
+ WHICH list it landed in — and a page may hold more than one. The default is translated
8738
+ so every consumer gets a named tab list without passing anything.
8739
+ (No backticks in this comment: the template is a template literal.) -->
8740
+ <TabList class="w-full" [variant]="variant()" [attr.aria-label]="ariaLabel() ?? ('navbarTabs.tabs' | transloco)" [(selectedTab)]="selectedTab" (selectedTabChange)="onSelectTab($event)" selectionMode="explicit" [softDisabled]="false">
8741
+ @for (tab of nav.tabs(); track $index) {
8742
+ <Tab
8743
+ [variant]="variant()"
8744
+ [class]="cn('group', !noTruncate() && 'basis-50', !isPill() && 'h-11 px-(--spacing-xs) text-(length:--size-base) font-semibold')"
8745
+ [value]="tab.path"
8746
+ overflowItem
8747
+ [noTruncate]="noTruncate()"
8748
+ [style.--tab-min-width]="tabMinWidth(!!tab.icon)"
8749
+ [disabled]="isTabDisabled(tab) || !isTabVisible($index)"
8750
+ >
8751
+ <div [class]="cn('flex items-center content-start w-full gap-1', !noTruncate() && '@container overflow-hidden min-w-0')">
8752
+ @if (tab.icon) {
8753
+ <!-- the icon never shrinks: the label truncates first -->
8754
+ <FaIcon [faClass]="tab.icon" class="shrink-0" aria-hidden="true" />
8755
+ }
8756
+ <!-- when the tab gets too narrow to show anything meaningful next to
8757
+ the icon, hide the label entirely instead of a clipped sliver.
8758
+ w-0 + invisible (not display: none) keeps the label's line box,
8759
+ so the tab height doesn't collapse to the icon's height -->
8760
+ <span [class]="cn(!noTruncate() && 'truncate', !noTruncate() && tab.icon && '@max-[3rem]:w-0 @max-[3rem]:invisible')">{{ tab.display | syslang | transloco }}</span>
8761
+ <!-- Show count badge only if count is > 0 -->
8762
+ @if((tab.count ?? 0) > 0) {
8763
+ <Badge
8764
+ variant="number"
8765
+ fill="secondary"
8766
+ scheme="sage"
8767
+ size="xs"
8768
+ [aria-label]="tab.count + ' ' + (tab.count === 1 ? ('navbarTabs.result' | transloco) : ('navbarTabs.results' | transloco))">
8769
+ {{ tab.count }}
8770
+ </Badge>
8771
+ }
8772
+ </div>
8773
+ </Tab>
8774
+ }
8775
+ <!-- Pill strip: ms-auto pins the trigger to the right end of the strip. Underline variants: no pin, the trigger
8776
+ follows the last visible tab, which is where the eye expects the rest of the list to be. The overflow
8777
+ manager reads only its size, never its position, so both placements measure the same.
8778
+
8779
+ The overflow trigger belongs INSIDE the tab list, and it is a tab: a tablist may only
8780
+ contain tab elements, so a plain button here would break the announced tab count and
8781
+ the arrow-key navigation. ngTab gives it the role and enrols it in the roving tabindex
8782
+ — so the list keeps ONE tab stop and the arrows reach it like any other tab.
8783
+
8784
+ It stays a real <button> because that is what menuTrigger needs: it wires the menu
8785
+ through the native popovertarget attribute, which only works on a button.
8786
+
8787
+ ms-auto pins it to the right end of the flex tab list. overflowStop is found by content
8788
+ query, so the measurement is unaffected by where it sits. Always mounted so the stop
8789
+ position is stable; hidden when every tab fits.
8790
+ (No backticks in this comment: the template is a template literal.) -->
8791
+ <button
8792
+ ngTab
8793
+ [value]="MORE_TABS_VALUE"
8794
+ [menuTrigger]="moreTabsMenu"
8795
+ variant="tertiary" [iconOnly]="true"
8796
+ [attr.aria-label]="'navbarTabs.moreTabs' | transloco" [tooltip]="'navbarTabs.moreTabs' | transloco" overflowStop
8797
+ [disabled]="moreTabs().length === 0"
8798
+ [class]="cn('shrink-0', isPill() ? 'ms-auto' : 'self-center')"
8799
+ [class.invisible]="moreTabs().length === 0">
8800
+ <EllipsisIcon [orientation]="isPill() ? 'vertical' : 'horizontal'"></EllipsisIcon>
8749
8801
  <span class="sr-only">{{ "navbarTabs.moreTabs" | transloco }}</span>
8750
8802
  </button>
8751
8803
  </TabList>
@@ -8802,6 +8854,14 @@ i0.ɵɵngDeclareClassMetadata({
8802
8854
  required: false
8803
8855
  }]
8804
8856
  }],
8857
+ variant: [{
8858
+ type: i0.Input,
8859
+ args: [{
8860
+ isSignal: true,
8861
+ alias: "variant",
8862
+ required: false
8863
+ }]
8864
+ }],
8805
8865
  showCount: [{
8806
8866
  type: i0.Input,
8807
8867
  args: [{
@@ -24533,8 +24593,8 @@ var AggregationDateComponent = class AggregationDateComponent {
24533
24593
  host: { properties: { "class": "cn(\"@container flex h-[inherit] max-h-[inherit] flex-col\", class(), isEmpty() && \"hidden\")" } },
24534
24594
  providers: [provideTranslocoScope("filters")],
24535
24595
  ngImport: i0,
24536
- 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 chevron is the keyboard stop, see the chevron below. Clear and apply are not in the\n header: they are in the body, in a bar at the top of the facet. -->\n<ng-template #content>\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\n class=\"flex min-h-0 flex-1 flex-col\"\n (keydown.enter)=\"onOptionEnter($event)\">\n <!-- The same bar as for the list and the tree, in a popover and in the filters drawer alike: clear (a button with a text, there but disabled when there is nothing\n to clear) and, while a choice is pending, apply, without an icon. There is no search field here to put them under:\n they sit at the top of the facet. -->\n <div class=\"flex shrink-0 items-center justify-end gap-1 pt-1 pl-1\">\n <button\n type=\"button\"\n variant=\"tertiary\"\n size=\"sm\"\n class=\"shrink-0\"\n [disabled]=\"!hasFilters() && !selection()\"\n [attr.aria-label]=\"'filters.clearFilters' | transloco\"\n (click)=\"clear()\">\n {{ \"filters.clearAll\" | transloco }}\n </button>\n @if (\n (!isControlled() || emitOn() === \"apply\") &&\n selection() &&\n validSelection()\n ) {\n <button\n type=\"button\"\n variant=\"accent\"\n size=\"sm\"\n class=\"shrink-0 overflow-hidden\"\n (click)=\"apply()\">\n <span class=\"truncate\">{{ \"filters.apply\" | transloco }}</span>\n </button>\n }\n </div>\n <div\n class=\"flex min-h-0 flex-1 flex-col\"\n role=\"group\"\n [attr.aria-label]=\"groupName ? (groupName | syslang | transloco) : null\">\n <ul\n class=\"flex min-h-0 flex-1 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</ng-template>\n\n@if (headless()) {\n <div class=\"flex min-h-0 flex-1 flex-col\">\n <ng-container [ngTemplateOutlet]=\"content\" />\n </div>\n} @else {\n <details\n [attr.open]=\"expanded()\"\n [attr.name]=\"id()\"\n class=\"group flex min-h-0 flex-1 flex-col space-y-2 details-content:flex details-content:min-h-0 details-content:flex-1 details-content:flex-col\">\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 shrink-0 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 (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 <ng-container [ngTemplateOutlet]=\"content\" />\n </details>\n}\n",
24537
- styles: [":host{min-width:200px}ul[role=list]{scrollbar-width:thin}\n"],
24596
+ 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 chevron is the keyboard stop, see the chevron below. Clear and apply are not in the\n header: they are in the body, in a bar at the top of the facet. -->\n<ng-template #content>\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\n class=\"flex min-h-0 flex-1 flex-col\"\n (keydown.enter)=\"onOptionEnter($event)\">\n <!-- The same bar as for the list and the tree, in a popover and in the filters drawer alike: clear (a button with a text, there but disabled when there is nothing\n to clear) and, while a choice is pending, apply, without an icon. There is no search field here to put them under:\n they sit at the top of the facet. -->\n <div class=\"flex shrink-0 items-center justify-end gap-1 pt-1 pl-1\">\n <button\n type=\"button\"\n variant=\"tertiary\"\n size=\"sm\"\n class=\"shrink-0\"\n [disabled]=\"!hasFilters() && !selection()\"\n [attr.aria-label]=\"'filters.clearFilters' | transloco\"\n (click)=\"clear()\">\n {{ \"filters.clearAll\" | transloco }}\n </button>\n @if (\n (!isControlled() || emitOn() === \"apply\") &&\n selection() &&\n validSelection()\n ) {\n <button\n type=\"button\"\n variant=\"accent\"\n size=\"sm\"\n class=\"shrink-0 overflow-hidden\"\n (click)=\"apply()\">\n <span class=\"truncate\">{{ \"filters.apply\" | transloco }}</span>\n </button>\n }\n </div>\n <div\n class=\"flex min-h-0 flex-1 flex-col\"\n role=\"group\"\n [attr.aria-label]=\"groupName ? (groupName | syslang | transloco) : null\">\n <ul\n class=\"flex min-h-0 flex-1 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</ng-template>\n\n@if (headless()) {\n <div class=\"flex min-h-0 flex-1 flex-col\">\n <ng-container [ngTemplateOutlet]=\"content\" />\n </div>\n} @else {\n <details\n [attr.open]=\"expanded()\"\n [attr.name]=\"id()\"\n class=\"group flex min-h-0 flex-1 flex-col space-y-2 details-content:flex details-content:min-h-0 details-content:flex-col\">\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 shrink-0 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 (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 <ng-container [ngTemplateOutlet]=\"content\" />\n </details>\n}\n",
24597
+ styles: [":host{interpolate-size:allow-keywords}details::details-content{flex:1 1 auto;block-size:0;overflow:clip;overflow-clip-margin:.25rem;transition:block-size .2s cubic-bezier(.32,.72,0,1),content-visibility .2s allow-discrete}details[open]::details-content{block-size:auto}@media(prefers-reduced-motion:reduce){details::details-content{transition:none}}\n", ":host{min-width:200px}ul[role=list]{scrollbar-width:thin}\n"],
24538
24598
  dependencies: [
24539
24599
  {
24540
24600
  kind: "directive",
@@ -24665,8 +24725,8 @@ i0.ɵɵngDeclareClassMetadata({
24665
24725
  TooltipDirective
24666
24726
  ],
24667
24727
  host: { "[class]": "cn(\"@container flex h-[inherit] max-h-[inherit] flex-col\", class(), isEmpty() && \"hidden\")" },
24668
- 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 chevron is the keyboard stop, see the chevron below. Clear and apply are not in the\n header: they are in the body, in a bar at the top of the facet. -->\n<ng-template #content>\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\n class=\"flex min-h-0 flex-1 flex-col\"\n (keydown.enter)=\"onOptionEnter($event)\">\n <!-- The same bar as for the list and the tree, in a popover and in the filters drawer alike: clear (a button with a text, there but disabled when there is nothing\n to clear) and, while a choice is pending, apply, without an icon. There is no search field here to put them under:\n they sit at the top of the facet. -->\n <div class=\"flex shrink-0 items-center justify-end gap-1 pt-1 pl-1\">\n <button\n type=\"button\"\n variant=\"tertiary\"\n size=\"sm\"\n class=\"shrink-0\"\n [disabled]=\"!hasFilters() && !selection()\"\n [attr.aria-label]=\"'filters.clearFilters' | transloco\"\n (click)=\"clear()\">\n {{ \"filters.clearAll\" | transloco }}\n </button>\n @if (\n (!isControlled() || emitOn() === \"apply\") &&\n selection() &&\n validSelection()\n ) {\n <button\n type=\"button\"\n variant=\"accent\"\n size=\"sm\"\n class=\"shrink-0 overflow-hidden\"\n (click)=\"apply()\">\n <span class=\"truncate\">{{ \"filters.apply\" | transloco }}</span>\n </button>\n }\n </div>\n <div\n class=\"flex min-h-0 flex-1 flex-col\"\n role=\"group\"\n [attr.aria-label]=\"groupName ? (groupName | syslang | transloco) : null\">\n <ul\n class=\"flex min-h-0 flex-1 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</ng-template>\n\n@if (headless()) {\n <div class=\"flex min-h-0 flex-1 flex-col\">\n <ng-container [ngTemplateOutlet]=\"content\" />\n </div>\n} @else {\n <details\n [attr.open]=\"expanded()\"\n [attr.name]=\"id()\"\n class=\"group flex min-h-0 flex-1 flex-col space-y-2 details-content:flex details-content:min-h-0 details-content:flex-1 details-content:flex-col\">\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 shrink-0 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 (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 <ng-container [ngTemplateOutlet]=\"content\" />\n </details>\n}\n",
24669
- styles: [":host{min-width:200px}ul[role=list]{scrollbar-width:thin}\n"]
24728
+ 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 chevron is the keyboard stop, see the chevron below. Clear and apply are not in the\n header: they are in the body, in a bar at the top of the facet. -->\n<ng-template #content>\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\n class=\"flex min-h-0 flex-1 flex-col\"\n (keydown.enter)=\"onOptionEnter($event)\">\n <!-- The same bar as for the list and the tree, in a popover and in the filters drawer alike: clear (a button with a text, there but disabled when there is nothing\n to clear) and, while a choice is pending, apply, without an icon. There is no search field here to put them under:\n they sit at the top of the facet. -->\n <div class=\"flex shrink-0 items-center justify-end gap-1 pt-1 pl-1\">\n <button\n type=\"button\"\n variant=\"tertiary\"\n size=\"sm\"\n class=\"shrink-0\"\n [disabled]=\"!hasFilters() && !selection()\"\n [attr.aria-label]=\"'filters.clearFilters' | transloco\"\n (click)=\"clear()\">\n {{ \"filters.clearAll\" | transloco }}\n </button>\n @if (\n (!isControlled() || emitOn() === \"apply\") &&\n selection() &&\n validSelection()\n ) {\n <button\n type=\"button\"\n variant=\"accent\"\n size=\"sm\"\n class=\"shrink-0 overflow-hidden\"\n (click)=\"apply()\">\n <span class=\"truncate\">{{ \"filters.apply\" | transloco }}</span>\n </button>\n }\n </div>\n <div\n class=\"flex min-h-0 flex-1 flex-col\"\n role=\"group\"\n [attr.aria-label]=\"groupName ? (groupName | syslang | transloco) : null\">\n <ul\n class=\"flex min-h-0 flex-1 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</ng-template>\n\n@if (headless()) {\n <div class=\"flex min-h-0 flex-1 flex-col\">\n <ng-container [ngTemplateOutlet]=\"content\" />\n </div>\n} @else {\n <details\n [attr.open]=\"expanded()\"\n [attr.name]=\"id()\"\n class=\"group flex min-h-0 flex-1 flex-col space-y-2 details-content:flex details-content:min-h-0 details-content:flex-col\">\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 shrink-0 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 (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 <ng-container [ngTemplateOutlet]=\"content\" />\n </details>\n}\n",
24729
+ styles: [":host{interpolate-size:allow-keywords}details::details-content{flex:1 1 auto;block-size:0;overflow:clip;overflow-clip-margin:.25rem;transition:block-size .2s cubic-bezier(.32,.72,0,1),content-visibility .2s allow-discrete}details[open]::details-content{block-size:auto}@media(prefers-reduced-motion:reduce){details::details-content{transition:none}}\n", ":host{min-width:200px}ul[role=list]{scrollbar-width:thin}\n"]
24670
24730
  }]
24671
24731
  }],
24672
24732
  ctorParameters: () => [],
@@ -24869,12 +24929,30 @@ var AggregationPanelComponent = class AggregationPanelComponent {
24869
24929
  isCollapsed = linkedSignal(() => this.collapsed(), ...ngDevMode ? [{ debugName: "isCollapsed" }] : /* istanbul ignore next */ []);
24870
24930
  expanded = computed(() => this.isCollapsed() ? null : "", ...ngDevMode ? [{ debugName: "expanded" }] : /* istanbul ignore next */ []);
24871
24931
  searchBox = viewChild(SearchInputComponent, ...ngDevMode ? [{ debugName: "searchBox" }] : /* istanbul ignore next */ []);
24932
+ searchToggle = viewChild("searchToggle", ...ngDevMode ? [{ debugName: "searchToggle" }] : /* istanbul ignore next */ []);
24933
+ injector = inject(Injector);
24872
24934
  isInPopover = !!inject(PopoverDirective, { optional: true });
24873
- constructor() {
24874
- effect(() => {
24875
- const searchBox = this.searchBox();
24876
- if (this.isInPopover && searchBox && this.expanded() !== null) setTimeout(() => searchBox.searchInput().nativeElement.focus(), 0);
24877
- });
24935
+ static nextId = 0;
24936
+ searchId = `aggregation-search-${AggregationPanelComponent.nextId++}`;
24937
+ searchRequested = signal(false, ...ngDevMode ? [{ debugName: "searchRequested" }] : /* istanbul ignore next */ []);
24938
+ canSearch = computed(() => !!this.aggregation()?.searchable && this.itemsLength() > 0, ...ngDevMode ? [{ debugName: "canSearch" }] : /* istanbul ignore next */ []);
24939
+ showSearch = computed(() => this.canSearch() && (this.searchRequested() || this.searchText() !== ""), ...ngDevMode ? [{ debugName: "showSearch" }] : /* istanbul ignore next */ []);
24940
+ toggleSearch(event) {
24941
+ event?.stopPropagation();
24942
+ event?.preventDefault();
24943
+ if (this.showSearch()) {
24944
+ this.searchText.set("");
24945
+ this.searchRequested.set(false);
24946
+ afterNextRender(() => this.searchToggle()?.nativeElement.focus(), { injector: this.injector });
24947
+ return;
24948
+ }
24949
+ this.searchRequested.set(true);
24950
+ afterNextRender(() => this.searchBox()?.searchInput().nativeElement.focus(), { injector: this.injector });
24951
+ }
24952
+ onSearchEscape(event) {
24953
+ if (!this.showSearch()) return;
24954
+ event.stopPropagation();
24955
+ this.toggleSearch();
24878
24956
  }
24879
24957
  onHeaderClick(event) {
24880
24958
  if (!this.isDate() && this.isEmpty()) {
@@ -25039,9 +25117,16 @@ var AggregationPanelComponent = class AggregationPanelComponent {
25039
25117
  predicate: SearchInputComponent,
25040
25118
  descendants: true,
25041
25119
  isSignal: true
25120
+ }, {
25121
+ propertyName: "searchToggle",
25122
+ first: true,
25123
+ predicate: ["searchToggle"],
25124
+ descendants: true,
25125
+ isSignal: true
25042
25126
  }],
25043
25127
  ngImport: i0,
25044
- template: "<!-- `<details>`/`<summary>` stay native (open/close, the `name`-based exclusive accordion): a facet is\n a disclosure, not a grid row. They used to sit in an `@angular/aria` grid so ArrowUp/ArrowDown could\n jump from one facet header to the next, but a grid without headers reads as a layout table\n (RGAA 5.3). The `<summary>` itself is `tabindex=\"-1\"`: Tab walks the header's chevron, never the header row, and the\n chevron is the keyboard way to collapse. Select all, clear and apply are not in the header: they are in the body, in a\n bar under the search field. Label, header actions (the count and the chevron) and body are `<ng-template>`s: `<ng-content select=\"…\">` can only be\n declared once, and the body is projected content. -->\n\n<ng-template #labelSlot>\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 truncate\">{{\n aggregation()?.display | syslang | transloco\n }}</span>\n </ng-content>\n</ng-template>\n\n<ng-template #headerActionsSlot>\n @if (showFiltersCount() && filtersCount() > 0) {\n <Badge variant=\"number\" fill=\"primary\" scheme=\"sage\" size=\"xs\" class=\"ml-1\">\n {{ filtersCount() }}\n </Badge>\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 [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 <span class=\"sr-only\">{{ \"filters.toggle\" | transloco }}</span>\n </button>\n }\n</ng-template>\n\n<ng-template #bodySlot>\n <div class=\"flex min-h-0 flex-1 flex-col\">\n @if (aggregation()?.searchable && itemsLength() > 0) {\n <!-- No `hotkey`: this field is one of many facet search boxes on the page, so it must not\n advertise a global shortcut. It used to pass `''` to neutralize SearchInput's old\n `\"shift+/\"` default, which left an empty `aria-keyshortcuts` attribute behind; the default\n is gone, so omitting the input is enough. -->\n <!-- A placeholder is not a label (RGAA 11.1): it goes away on the first keystroke, and it is\n the same word in every facet panel of the page. The name says WHICH facet this box\n filters — the one thing that tells a dozen identical fields apart. -->\n <!-- mt-2: without it, the search box sits flush under <summary>, and the header's focus ring reads as touching it.\n Headless has no <summary>, so nothing to keep apart from: the popover's own space (its resize handle, or the header of\n the \"more filters\" facet) is enough. SearchInput also hands its class to its inner field, so the margin counts twice. -->\n <SearchInput\n [class]=\"headless() ? 'mb-1 shrink-0' : 'mt-2 mb-1 shrink-0'\"\n [placeholder]=\"'search' | transloco\"\n [clearLabel]=\"'filters.clearSearch' | transloco\"\n [aria-label]=\"\n 'filters.searchInFacet'\n | transloco\n : { facet: (aggregation()?.display | syslang | transloco) }\n \"\n [(value)]=\"searchText\">\n <ng-content select=\"[search-addon]\" />\n </SearchInput>\n }\n\n @if (itemsLength() > 0 || hasFilters() || selection()) {\n <!-- The bar under the search field, in a popover and in the filters drawer alike: select all (the count of selected items in its tooltip), then clear and apply. Clear is a button\n with a text, not an icon, and stays in place (disabled) when there is nothing to clear. Apply only shows once\n something is pending, as it always did, and sits at the same level. The checkbox keeps its own name: the text\n beside it is a count, not the label of what it does. -->\n <div class=\"mb-1 flex shrink-0 items-center gap-1 pl-1.5\">\n <!-- How many are selected is in the tooltip of the checkbox, not written beside it: that room is what clear and apply need.\n Screen readers keep it as text (the tooltip only shows on hover and focus, and describes). -->\n <span\n class=\"inline-flex shrink-0\"\n [tooltip]=\"\n 'filters.selectedCount' | transloco: { count: selectedCount() }\n \"\n tooltipPosition=\"bottom\">\n <checkbox\n [checked]=\"isAllSelected()\"\n [indeterminate]=\"selectedCount() > 0 && !isAllSelected()\"\n [aria-label]=\"'filters.selectAllFilters' | transloco\"\n (checkedChange)=\"\n $event ? allSelected.emit() : allUnselected.emit()\n \" />\n </span>\n <span class=\"sr-only\" aria-live=\"polite\">{{\n \"filters.selectedCount\" | transloco: { count: selectedCount() }\n }}</span>\n <span class=\"flex-1\" aria-hidden=\"true\"></span>\n <button\n type=\"button\"\n variant=\"tertiary\"\n size=\"sm\"\n class=\"shrink-0\"\n [disabled]=\"!hasFilters() && selectedCount() === 0\"\n [attr.aria-label]=\"'filters.clearFilters' | transloco\"\n (click)=\"cleared.emit()\">\n {{ \"filters.clearAll\" | transloco }}\n </button>\n @if (selection()) {\n <!-- No icon: the room is short, and the word says it. -->\n <button\n type=\"button\"\n variant=\"accent\"\n size=\"sm\"\n class=\"shrink-0 overflow-hidden\"\n (click)=\"applied.emit()\">\n <span class=\"truncate\">{{ \"filters.apply\" | transloco }}</span>\n </button>\n }\n </div>\n }\n\n <ng-content />\n\n @if (hasMore() && searchedItemsLength() === 0) {\n <button\n type=\"button\"\n class=\"mt-1 flex w-full shrink-0 justify-center\"\n [attr.aria-label]=\"'loadMore' | transloco\"\n (click)=\"loadedMore.emit()\">\n {{ \"loadMore\" | transloco }}\n </button>\n }\n </div>\n</ng-template>\n\n@if (headless()) {\n <div class=\"flex min-h-0 flex-1 flex-col p-0.5\">\n <ng-container [ngTemplateOutlet]=\"bodySlot\" />\n </div>\n} @else {\n <details\n [attr.open]=\"expanded()\"\n [attr.name]=\"id()\"\n class=\"group flex min-h-0 flex-1 flex-col p-0.5 details-content:flex details-content:min-h-0 details-content:flex-1 details-content:flex-col\"\n (toggle)=\"onToggle($event)\">\n <summary\n tabindex=\"-1\"\n [class.cursor-pointer]=\"collapsible() && !isEmpty()\"\n [class]=\"\n (isEmpty() ? 'text-(--font-neutral-muted) ' : '') + rowStateClass\n \"\n class=\"m-0 flex h-8 w-full shrink-0 scroll-mt-16 scroll-mb-3 items-center gap-1 pl-1 font-semibold select-none\"\n (click)=\"onHeaderClick($event)\">\n <ng-container [ngTemplateOutlet]=\"labelSlot\" />\n <ng-container [ngTemplateOutlet]=\"headerActionsSlot\" />\n </summary>\n <ng-container [ngTemplateOutlet]=\"bodySlot\" />\n </details>\n}\n",
25128
+ template: "<!-- `<details>`/`<summary>` stay native (open/close, the `name`-based exclusive accordion): a facet is\n a disclosure, not a grid row. They used to sit in an `@angular/aria` grid so ArrowUp/ArrowDown could\n jump from one facet header to the next, but a grid without headers reads as a layout table\n (RGAA 5.3). The `<summary>` itself is `tabindex=\"-1\"`: Tab walks the header's chevron, never the header row, and the\n chevron is the keyboard way to collapse. Select all, clear and apply are not in the header: they are in the body, in a\n bar under the search field. Label, header actions (the count and the chevron) and body are `<ng-template>`s: `<ng-content select=\"…\">` can only be\n declared once, and the body is projected content. -->\n\n<ng-template #searchToggleSlot>\n <button\n #searchToggle\n type=\"button\"\n variant=\"tertiary\"\n [iconOnly]=\"true\"\n size=\"sm\"\n class=\"shrink-0 cursor-pointer\"\n [attr.aria-expanded]=\"showSearch()\"\n [attr.aria-controls]=\"searchId\"\n [tooltip]=\"\n 'filters.searchInFacet'\n | transloco: { facet: (aggregation()?.display | syslang | transloco) }\n \"\n (click)=\"toggleSearch($event)\">\n <search-icon class=\"shrink-0\" />\n <span class=\"sr-only\">{{\n \"filters.searchInFacet\"\n | transloco: { facet: (aggregation()?.display | syslang | transloco) }\n }}</span>\n </button>\n</ng-template>\n\n<ng-template #labelSlot>\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 truncate\">{{\n aggregation()?.display | syslang | transloco\n }}</span>\n </ng-content>\n</ng-template>\n\n<ng-template #headerActionsSlot>\n @if (showFiltersCount() && filtersCount() > 0) {\n <Badge variant=\"number\" fill=\"primary\" scheme=\"sage\" size=\"xs\" class=\"ml-1\">\n {{ filtersCount() }}\n </Badge>\n }\n @if (canSearch() && !isCollapsed()) {\n <!-- The magnifier opens the search field of the facet (and closes it): the field is not on screen until then. It is\n in the header, next to the chevron, and only while the facet is open — there is nothing to search in a closed\n one. Interactive content in a summary does not toggle the details by itself, and the click is stopped anyway. -->\n <ng-container [ngTemplateOutlet]=\"searchToggleSlot\" />\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 [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 <span class=\"sr-only\">{{ \"filters.toggle\" | transloco }}</span>\n </button>\n }\n</ng-template>\n\n<ng-template #bodySlot>\n <div class=\"flex min-h-0 flex-1 flex-col\">\n @if (canSearch()) {\n <!-- The field is always in the DOM and folded to nothing until it is asked for: a grid row that goes from 0fr to 1fr is\n the one height animation that needs no measuring. `inert` takes the folded field out of the tab order and of the\n accessibility tree. The margins live in the inner box: a padding on the folded element itself would keep it\n from reaching zero.\n\n No `hotkey`: this field is one of many facet search boxes on the page, so it must not advertise a global shortcut.\n A placeholder is not a label (RGAA 11.1): the name says WHICH facet this box filters — the one thing that tells a\n dozen identical fields apart. SearchInput hands its class to its inner field, so a margin on it would count twice,\n which is one more reason for the padding on the box around it. -->\n <div\n [id]=\"searchId\"\n class=\"grid shrink-0 transition-[grid-template-rows,opacity] duration-200 ease-[cubic-bezier(0.32,0.72,0,1)] motion-reduce:transition-none\"\n [class]=\"\n showSearch()\n ? 'grid-rows-[1fr] opacity-100'\n : 'grid-rows-[0fr] opacity-0'\n \"\n [inert]=\"!showSearch()\">\n <!-- `-mx-1 px-1`: the box clips what is outside it, and the focus ring of the field is drawn outside the field,\n so it needs a few pixels of room on each side — taken back by the negative margin so nothing moves. -->\n <div class=\"-mx-1 min-h-0 overflow-hidden px-1\">\n <div [class]=\"headless() ? 'pt-0.5 pb-1' : 'pt-2 pb-1'\">\n <SearchInput\n [placeholder]=\"'search' | transloco\"\n [clearLabel]=\"'filters.clearSearch' | transloco\"\n [aria-label]=\"\n 'filters.searchInFacet'\n | transloco\n : { facet: (aggregation()?.display | syslang | transloco) }\n \"\n [(value)]=\"searchText\"\n (keydown.escape)=\"onSearchEscape($event)\">\n <ng-content select=\"[search-addon]\" />\n </SearchInput>\n </div>\n </div>\n </div>\n }\n\n @if (itemsLength() > 0 || hasFilters() || selection()) {\n <!-- The bar under the search field, in a popover and in the filters drawer alike: select all (the count of selected items in its tooltip), then clear and apply. Clear is a button\n with a text, not an icon, and stays in place (disabled) when there is nothing to clear. Apply only shows once\n something is pending, as it always did, and sits at the same level. The checkbox keeps its own name: the text\n beside it is a count, not the label of what it does. -->\n <div class=\"mb-1 flex shrink-0 items-center gap-1 pl-1.5\">\n <!-- How many are selected is in the tooltip of the checkbox, not written beside it: that room is what clear and apply need.\n Screen readers keep it as text (the tooltip only shows on hover and focus, and describes). -->\n <span\n class=\"inline-flex shrink-0\"\n [tooltip]=\"\n 'filters.selectedCount' | transloco: { count: selectedCount() }\n \"\n tooltipPosition=\"bottom\">\n <checkbox\n [checked]=\"isAllSelected()\"\n [indeterminate]=\"selectedCount() > 0 && !isAllSelected()\"\n [aria-label]=\"'filters.selectAllFilters' | transloco\"\n (checkedChange)=\"\n $event ? allSelected.emit() : allUnselected.emit()\n \" />\n </span>\n <span class=\"sr-only\" aria-live=\"polite\">{{\n \"filters.selectedCount\" | transloco: { count: selectedCount() }\n }}</span>\n <span class=\"flex-1\" aria-hidden=\"true\"></span>\n @if (headless() && canSearch()) {\n <ng-container [ngTemplateOutlet]=\"searchToggleSlot\" />\n }\n <button\n type=\"button\"\n variant=\"tertiary\"\n size=\"sm\"\n class=\"shrink-0\"\n [disabled]=\"!hasFilters() && selectedCount() === 0\"\n [attr.aria-label]=\"'filters.clearFilters' | transloco\"\n (click)=\"cleared.emit()\">\n {{ \"filters.clearAll\" | transloco }}\n </button>\n @if (selection()) {\n <!-- No icon: the room is short, and the word says it. -->\n <button\n type=\"button\"\n variant=\"accent\"\n size=\"sm\"\n class=\"shrink-0 overflow-hidden\"\n (click)=\"applied.emit()\">\n <span class=\"truncate\">{{ \"filters.apply\" | transloco }}</span>\n </button>\n }\n </div>\n }\n\n <ng-content />\n\n @if (hasMore() && searchedItemsLength() === 0) {\n <button\n type=\"button\"\n class=\"mt-1 flex w-full shrink-0 justify-center\"\n [attr.aria-label]=\"'loadMore' | transloco\"\n (click)=\"loadedMore.emit()\">\n {{ \"loadMore\" | transloco }}\n </button>\n }\n </div>\n</ng-template>\n\n@if (headless()) {\n <div class=\"flex min-h-0 flex-1 flex-col p-0.5\">\n <ng-container [ngTemplateOutlet]=\"bodySlot\" />\n </div>\n} @else {\n <details\n [attr.open]=\"expanded()\"\n [attr.name]=\"id()\"\n class=\"group flex min-h-0 flex-1 flex-col p-0.5 details-content:flex details-content:min-h-0 details-content:flex-col\"\n (toggle)=\"onToggle($event)\">\n <summary\n tabindex=\"-1\"\n [class.cursor-pointer]=\"collapsible() && !isEmpty()\"\n [class]=\"\n (isEmpty() ? 'text-(--font-neutral-muted) ' : '') + rowStateClass\n \"\n class=\"m-0 flex h-8 w-full shrink-0 scroll-mt-16 scroll-mb-3 items-center gap-1 pl-1 font-semibold select-none\"\n (click)=\"onHeaderClick($event)\">\n <ng-container [ngTemplateOutlet]=\"labelSlot\" />\n <ng-container [ngTemplateOutlet]=\"headerActionsSlot\" />\n </summary>\n <ng-container [ngTemplateOutlet]=\"bodySlot\" />\n </details>\n}\n",
25129
+ styles: [":host{interpolate-size:allow-keywords}details::details-content{flex:1 1 auto;block-size:0;overflow:clip;overflow-clip-margin:.25rem;transition:block-size .2s cubic-bezier(.32,.72,0,1),content-visibility .2s allow-discrete}details[open]::details-content{block-size:auto}@media(prefers-reduced-motion:reduce){details::details-content{transition:none}}\n"],
25045
25130
  dependencies: [
25046
25131
  {
25047
25132
  kind: "directive",
@@ -25084,6 +25169,12 @@ var AggregationPanelComponent = class AggregationPanelComponent {
25084
25169
  selector: "chevron-right, chevron-right-icon, ChevronRight, ChevronRightIcon",
25085
25170
  inputs: ["class"]
25086
25171
  },
25172
+ {
25173
+ kind: "component",
25174
+ type: SearchIcon,
25175
+ selector: "search, search-icon, Search, SearchIcon",
25176
+ inputs: ["class"]
25177
+ },
25087
25178
  {
25088
25179
  kind: "component",
25089
25180
  type: SearchInputComponent,
@@ -25164,6 +25255,7 @@ i0.ɵɵngDeclareClassMetadata({
25164
25255
  TranslocoPipe,
25165
25256
  BadgeComponent,
25166
25257
  ChevronRightIcon,
25258
+ SearchIcon,
25167
25259
  SearchInputComponent,
25168
25260
  FaIconComponent,
25169
25261
  CheckboxComponent,
@@ -25171,10 +25263,10 @@ i0.ɵɵngDeclareClassMetadata({
25171
25263
  ],
25172
25264
  providers: [provideTranslocoScope("filters")],
25173
25265
  host: { class: "flex min-h-0 flex-1 flex-col" },
25174
- template: "<!-- `<details>`/`<summary>` stay native (open/close, the `name`-based exclusive accordion): a facet is\n a disclosure, not a grid row. They used to sit in an `@angular/aria` grid so ArrowUp/ArrowDown could\n jump from one facet header to the next, but a grid without headers reads as a layout table\n (RGAA 5.3). The `<summary>` itself is `tabindex=\"-1\"`: Tab walks the header's chevron, never the header row, and the\n chevron is the keyboard way to collapse. Select all, clear and apply are not in the header: they are in the body, in a\n bar under the search field. Label, header actions (the count and the chevron) and body are `<ng-template>`s: `<ng-content select=\"…\">` can only be\n declared once, and the body is projected content. -->\n\n<ng-template #labelSlot>\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 truncate\">{{\n aggregation()?.display | syslang | transloco\n }}</span>\n </ng-content>\n</ng-template>\n\n<ng-template #headerActionsSlot>\n @if (showFiltersCount() && filtersCount() > 0) {\n <Badge variant=\"number\" fill=\"primary\" scheme=\"sage\" size=\"xs\" class=\"ml-1\">\n {{ filtersCount() }}\n </Badge>\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 [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 <span class=\"sr-only\">{{ \"filters.toggle\" | transloco }}</span>\n </button>\n }\n</ng-template>\n\n<ng-template #bodySlot>\n <div class=\"flex min-h-0 flex-1 flex-col\">\n @if (aggregation()?.searchable && itemsLength() > 0) {\n <!-- No `hotkey`: this field is one of many facet search boxes on the page, so it must not\n advertise a global shortcut. It used to pass `''` to neutralize SearchInput's old\n `\"shift+/\"` default, which left an empty `aria-keyshortcuts` attribute behind; the default\n is gone, so omitting the input is enough. -->\n <!-- A placeholder is not a label (RGAA 11.1): it goes away on the first keystroke, and it is\n the same word in every facet panel of the page. The name says WHICH facet this box\n filters — the one thing that tells a dozen identical fields apart. -->\n <!-- mt-2: without it, the search box sits flush under <summary>, and the header's focus ring reads as touching it.\n Headless has no <summary>, so nothing to keep apart from: the popover's own space (its resize handle, or the header of\n the \"more filters\" facet) is enough. SearchInput also hands its class to its inner field, so the margin counts twice. -->\n <SearchInput\n [class]=\"headless() ? 'mb-1 shrink-0' : 'mt-2 mb-1 shrink-0'\"\n [placeholder]=\"'search' | transloco\"\n [clearLabel]=\"'filters.clearSearch' | transloco\"\n [aria-label]=\"\n 'filters.searchInFacet'\n | transloco\n : { facet: (aggregation()?.display | syslang | transloco) }\n \"\n [(value)]=\"searchText\">\n <ng-content select=\"[search-addon]\" />\n </SearchInput>\n }\n\n @if (itemsLength() > 0 || hasFilters() || selection()) {\n <!-- The bar under the search field, in a popover and in the filters drawer alike: select all (the count of selected items in its tooltip), then clear and apply. Clear is a button\n with a text, not an icon, and stays in place (disabled) when there is nothing to clear. Apply only shows once\n something is pending, as it always did, and sits at the same level. The checkbox keeps its own name: the text\n beside it is a count, not the label of what it does. -->\n <div class=\"mb-1 flex shrink-0 items-center gap-1 pl-1.5\">\n <!-- How many are selected is in the tooltip of the checkbox, not written beside it: that room is what clear and apply need.\n Screen readers keep it as text (the tooltip only shows on hover and focus, and describes). -->\n <span\n class=\"inline-flex shrink-0\"\n [tooltip]=\"\n 'filters.selectedCount' | transloco: { count: selectedCount() }\n \"\n tooltipPosition=\"bottom\">\n <checkbox\n [checked]=\"isAllSelected()\"\n [indeterminate]=\"selectedCount() > 0 && !isAllSelected()\"\n [aria-label]=\"'filters.selectAllFilters' | transloco\"\n (checkedChange)=\"\n $event ? allSelected.emit() : allUnselected.emit()\n \" />\n </span>\n <span class=\"sr-only\" aria-live=\"polite\">{{\n \"filters.selectedCount\" | transloco: { count: selectedCount() }\n }}</span>\n <span class=\"flex-1\" aria-hidden=\"true\"></span>\n <button\n type=\"button\"\n variant=\"tertiary\"\n size=\"sm\"\n class=\"shrink-0\"\n [disabled]=\"!hasFilters() && selectedCount() === 0\"\n [attr.aria-label]=\"'filters.clearFilters' | transloco\"\n (click)=\"cleared.emit()\">\n {{ \"filters.clearAll\" | transloco }}\n </button>\n @if (selection()) {\n <!-- No icon: the room is short, and the word says it. -->\n <button\n type=\"button\"\n variant=\"accent\"\n size=\"sm\"\n class=\"shrink-0 overflow-hidden\"\n (click)=\"applied.emit()\">\n <span class=\"truncate\">{{ \"filters.apply\" | transloco }}</span>\n </button>\n }\n </div>\n }\n\n <ng-content />\n\n @if (hasMore() && searchedItemsLength() === 0) {\n <button\n type=\"button\"\n class=\"mt-1 flex w-full shrink-0 justify-center\"\n [attr.aria-label]=\"'loadMore' | transloco\"\n (click)=\"loadedMore.emit()\">\n {{ \"loadMore\" | transloco }}\n </button>\n }\n </div>\n</ng-template>\n\n@if (headless()) {\n <div class=\"flex min-h-0 flex-1 flex-col p-0.5\">\n <ng-container [ngTemplateOutlet]=\"bodySlot\" />\n </div>\n} @else {\n <details\n [attr.open]=\"expanded()\"\n [attr.name]=\"id()\"\n class=\"group flex min-h-0 flex-1 flex-col p-0.5 details-content:flex details-content:min-h-0 details-content:flex-1 details-content:flex-col\"\n (toggle)=\"onToggle($event)\">\n <summary\n tabindex=\"-1\"\n [class.cursor-pointer]=\"collapsible() && !isEmpty()\"\n [class]=\"\n (isEmpty() ? 'text-(--font-neutral-muted) ' : '') + rowStateClass\n \"\n class=\"m-0 flex h-8 w-full shrink-0 scroll-mt-16 scroll-mb-3 items-center gap-1 pl-1 font-semibold select-none\"\n (click)=\"onHeaderClick($event)\">\n <ng-container [ngTemplateOutlet]=\"labelSlot\" />\n <ng-container [ngTemplateOutlet]=\"headerActionsSlot\" />\n </summary>\n <ng-container [ngTemplateOutlet]=\"bodySlot\" />\n </details>\n}\n"
25266
+ template: "<!-- `<details>`/`<summary>` stay native (open/close, the `name`-based exclusive accordion): a facet is\n a disclosure, not a grid row. They used to sit in an `@angular/aria` grid so ArrowUp/ArrowDown could\n jump from one facet header to the next, but a grid without headers reads as a layout table\n (RGAA 5.3). The `<summary>` itself is `tabindex=\"-1\"`: Tab walks the header's chevron, never the header row, and the\n chevron is the keyboard way to collapse. Select all, clear and apply are not in the header: they are in the body, in a\n bar under the search field. Label, header actions (the count and the chevron) and body are `<ng-template>`s: `<ng-content select=\"…\">` can only be\n declared once, and the body is projected content. -->\n\n<ng-template #searchToggleSlot>\n <button\n #searchToggle\n type=\"button\"\n variant=\"tertiary\"\n [iconOnly]=\"true\"\n size=\"sm\"\n class=\"shrink-0 cursor-pointer\"\n [attr.aria-expanded]=\"showSearch()\"\n [attr.aria-controls]=\"searchId\"\n [tooltip]=\"\n 'filters.searchInFacet'\n | transloco: { facet: (aggregation()?.display | syslang | transloco) }\n \"\n (click)=\"toggleSearch($event)\">\n <search-icon class=\"shrink-0\" />\n <span class=\"sr-only\">{{\n \"filters.searchInFacet\"\n | transloco: { facet: (aggregation()?.display | syslang | transloco) }\n }}</span>\n </button>\n</ng-template>\n\n<ng-template #labelSlot>\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 truncate\">{{\n aggregation()?.display | syslang | transloco\n }}</span>\n </ng-content>\n</ng-template>\n\n<ng-template #headerActionsSlot>\n @if (showFiltersCount() && filtersCount() > 0) {\n <Badge variant=\"number\" fill=\"primary\" scheme=\"sage\" size=\"xs\" class=\"ml-1\">\n {{ filtersCount() }}\n </Badge>\n }\n @if (canSearch() && !isCollapsed()) {\n <!-- The magnifier opens the search field of the facet (and closes it): the field is not on screen until then. It is\n in the header, next to the chevron, and only while the facet is open — there is nothing to search in a closed\n one. Interactive content in a summary does not toggle the details by itself, and the click is stopped anyway. -->\n <ng-container [ngTemplateOutlet]=\"searchToggleSlot\" />\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 [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 <span class=\"sr-only\">{{ \"filters.toggle\" | transloco }}</span>\n </button>\n }\n</ng-template>\n\n<ng-template #bodySlot>\n <div class=\"flex min-h-0 flex-1 flex-col\">\n @if (canSearch()) {\n <!-- The field is always in the DOM and folded to nothing until it is asked for: a grid row that goes from 0fr to 1fr is\n the one height animation that needs no measuring. `inert` takes the folded field out of the tab order and of the\n accessibility tree. The margins live in the inner box: a padding on the folded element itself would keep it\n from reaching zero.\n\n No `hotkey`: this field is one of many facet search boxes on the page, so it must not advertise a global shortcut.\n A placeholder is not a label (RGAA 11.1): the name says WHICH facet this box filters — the one thing that tells a\n dozen identical fields apart. SearchInput hands its class to its inner field, so a margin on it would count twice,\n which is one more reason for the padding on the box around it. -->\n <div\n [id]=\"searchId\"\n class=\"grid shrink-0 transition-[grid-template-rows,opacity] duration-200 ease-[cubic-bezier(0.32,0.72,0,1)] motion-reduce:transition-none\"\n [class]=\"\n showSearch()\n ? 'grid-rows-[1fr] opacity-100'\n : 'grid-rows-[0fr] opacity-0'\n \"\n [inert]=\"!showSearch()\">\n <!-- `-mx-1 px-1`: the box clips what is outside it, and the focus ring of the field is drawn outside the field,\n so it needs a few pixels of room on each side — taken back by the negative margin so nothing moves. -->\n <div class=\"-mx-1 min-h-0 overflow-hidden px-1\">\n <div [class]=\"headless() ? 'pt-0.5 pb-1' : 'pt-2 pb-1'\">\n <SearchInput\n [placeholder]=\"'search' | transloco\"\n [clearLabel]=\"'filters.clearSearch' | transloco\"\n [aria-label]=\"\n 'filters.searchInFacet'\n | transloco\n : { facet: (aggregation()?.display | syslang | transloco) }\n \"\n [(value)]=\"searchText\"\n (keydown.escape)=\"onSearchEscape($event)\">\n <ng-content select=\"[search-addon]\" />\n </SearchInput>\n </div>\n </div>\n </div>\n }\n\n @if (itemsLength() > 0 || hasFilters() || selection()) {\n <!-- The bar under the search field, in a popover and in the filters drawer alike: select all (the count of selected items in its tooltip), then clear and apply. Clear is a button\n with a text, not an icon, and stays in place (disabled) when there is nothing to clear. Apply only shows once\n something is pending, as it always did, and sits at the same level. The checkbox keeps its own name: the text\n beside it is a count, not the label of what it does. -->\n <div class=\"mb-1 flex shrink-0 items-center gap-1 pl-1.5\">\n <!-- How many are selected is in the tooltip of the checkbox, not written beside it: that room is what clear and apply need.\n Screen readers keep it as text (the tooltip only shows on hover and focus, and describes). -->\n <span\n class=\"inline-flex shrink-0\"\n [tooltip]=\"\n 'filters.selectedCount' | transloco: { count: selectedCount() }\n \"\n tooltipPosition=\"bottom\">\n <checkbox\n [checked]=\"isAllSelected()\"\n [indeterminate]=\"selectedCount() > 0 && !isAllSelected()\"\n [aria-label]=\"'filters.selectAllFilters' | transloco\"\n (checkedChange)=\"\n $event ? allSelected.emit() : allUnselected.emit()\n \" />\n </span>\n <span class=\"sr-only\" aria-live=\"polite\">{{\n \"filters.selectedCount\" | transloco: { count: selectedCount() }\n }}</span>\n <span class=\"flex-1\" aria-hidden=\"true\"></span>\n @if (headless() && canSearch()) {\n <ng-container [ngTemplateOutlet]=\"searchToggleSlot\" />\n }\n <button\n type=\"button\"\n variant=\"tertiary\"\n size=\"sm\"\n class=\"shrink-0\"\n [disabled]=\"!hasFilters() && selectedCount() === 0\"\n [attr.aria-label]=\"'filters.clearFilters' | transloco\"\n (click)=\"cleared.emit()\">\n {{ \"filters.clearAll\" | transloco }}\n </button>\n @if (selection()) {\n <!-- No icon: the room is short, and the word says it. -->\n <button\n type=\"button\"\n variant=\"accent\"\n size=\"sm\"\n class=\"shrink-0 overflow-hidden\"\n (click)=\"applied.emit()\">\n <span class=\"truncate\">{{ \"filters.apply\" | transloco }}</span>\n </button>\n }\n </div>\n }\n\n <ng-content />\n\n @if (hasMore() && searchedItemsLength() === 0) {\n <button\n type=\"button\"\n class=\"mt-1 flex w-full shrink-0 justify-center\"\n [attr.aria-label]=\"'loadMore' | transloco\"\n (click)=\"loadedMore.emit()\">\n {{ \"loadMore\" | transloco }}\n </button>\n }\n </div>\n</ng-template>\n\n@if (headless()) {\n <div class=\"flex min-h-0 flex-1 flex-col p-0.5\">\n <ng-container [ngTemplateOutlet]=\"bodySlot\" />\n </div>\n} @else {\n <details\n [attr.open]=\"expanded()\"\n [attr.name]=\"id()\"\n class=\"group flex min-h-0 flex-1 flex-col p-0.5 details-content:flex details-content:min-h-0 details-content:flex-col\"\n (toggle)=\"onToggle($event)\">\n <summary\n tabindex=\"-1\"\n [class.cursor-pointer]=\"collapsible() && !isEmpty()\"\n [class]=\"\n (isEmpty() ? 'text-(--font-neutral-muted) ' : '') + rowStateClass\n \"\n class=\"m-0 flex h-8 w-full shrink-0 scroll-mt-16 scroll-mb-3 items-center gap-1 pl-1 font-semibold select-none\"\n (click)=\"onHeaderClick($event)\">\n <ng-container [ngTemplateOutlet]=\"labelSlot\" />\n <ng-container [ngTemplateOutlet]=\"headerActionsSlot\" />\n </summary>\n <ng-container [ngTemplateOutlet]=\"bodySlot\" />\n </details>\n}\n",
25267
+ styles: [":host{interpolate-size:allow-keywords}details::details-content{flex:1 1 auto;block-size:0;overflow:clip;overflow-clip-margin:.25rem;transition:block-size .2s cubic-bezier(.32,.72,0,1),content-visibility .2s allow-discrete}details[open]::details-content{block-size:auto}@media(prefers-reduced-motion:reduce){details::details-content{transition:none}}\n"]
25175
25268
  }]
25176
25269
  }],
25177
- ctorParameters: () => [],
25178
25270
  propDecorators: {
25179
25271
  id: [{
25180
25272
  type: i0.Input,
@@ -25338,6 +25430,10 @@ i0.ɵɵngDeclareClassMetadata({
25338
25430
  searchBox: [{
25339
25431
  type: i0.ViewChild,
25340
25432
  args: [i0.forwardRef(() => SearchInputComponent), { isSignal: true }]
25433
+ }],
25434
+ searchToggle: [{
25435
+ type: i0.ViewChild,
25436
+ args: ["searchToggle", { isSignal: true }]
25341
25437
  }]
25342
25438
  }
25343
25439
  });
@@ -26038,7 +26134,7 @@ var AggregationTreeComponent = class AggregationTreeComponent {
26038
26134
  isSignal: true
26039
26135
  }],
26040
26136
  ngImport: i0,
26041
- template: "@if (!aggregation()?.isTree) {\n <div role=\"alert\" class=\"p-2 text-sm text-red-500\">\n <triangle-alert-icon class=\"mr-1 shrink-0\" />\n The aggregationTree component does not support list aggregations. Please use\n the &lt;Aggregation /&gt; component instead.\n </div>\n}\n<aggregation-panel\n [id]=\"id()\"\n [collapsible]=\"collapsible()\"\n [collapsed]=\"collapsed()\"\n [isEmpty]=\"isEmpty()\"\n [isDate]=\"isDate()\"\n [aggregation]=\"aggregation()\"\n [showFiltersCount]=\"showFiltersCount()\"\n [filtersCount]=\"filtersCount()\"\n [hasFilters]=\"hasFilters()\"\n [selection]=\"selection()\"\n [isAllSelected]=\"isAllSelected()\"\n [headless]=\"headless()\"\n [selectedCount]=\"selected().length\"\n [(searchText)]=\"searchText\"\n [itemsLength]=\"items().length\"\n [hasMore]=\"aggregation()?.$hasMore ?? false\"\n [searchedItemsLength]=\"searchedItems().length\"\n (cleared)=\"clear()\"\n (applied)=\"apply()\"\n (allSelected)=\"selectAll()\"\n (allUnselected)=\"unselectAll()\"\n (loadedMore)=\"loadMore()\">\n <!-- Relay the host's `label` projection into the panel's `label` slot. When the\n host projects nothing, the local <ng-content> falls back to the icon + display\n (mirrors AggregationPanel's own fallback), so the header is never empty. -->\n <ng-container ngProjectAs=\"label\">\n <ng-content select=\"label\">\n @let labelIcon = aggregation()?.icon;\n @if (labelIcon) {\n <FaIcon [faClass]=\"labelIcon\" class=\"mr-1 shrink-0\" />\n }\n <span class=\"grow truncate\">{{\n aggregation()?.display | syslang | transloco\n }}</span>\n </ng-content>\n </ng-container>\n <div class=\"min-h-0 w-full flex-1 scrollbar-thin overflow-auto pb-3\">\n <ul\n ngTree\n #tree=\"ngTree\"\n [multi]=\"true\"\n [(value)]=\"selected\"\n (valueChange)=\"onSelectionChange($event)\"\n /* Translated like the flat list's own label (aggregation-list.html): this was the raw\n `display`, so the accessible name of a whole facet came out in the source language.\n The pipes apply to the whole nullish chain, `|` being the loosest operator Angular\n parses — no parentheses needed. */\n [attr.aria-label]=\"aggregation()?.display ?? name() | syslang | transloco\"\n class=\"basic-tree m-1\">\n <ng-template\n [ngTemplateOutlet]=\"treeNodes\"\n [ngTemplateOutletContext]=\"{ nodes: nodes(), parent: tree }\" />\n </ul>\n </div>\n</aggregation-panel>\n\n<ng-template #treeNodes let-nodes=\"nodes\" let-parent=\"parent\">\n @for (node of nodes; track node.$path) {\n @let label = node.display ?? node.value ?? \"\";\n <li\n ngTreeItem\n #treeItem=\"ngTreeItem\"\n [parent]=\"parent\"\n [value]=\"node.$path\"\n [label]=\"label\"\n [disabled]=\"node.disabled\"\n [(expanded)]=\"node.$opened\"\n (expandedChange)=\"onExpandedChange(node, $event)\"\n [attr.aria-label]=\"label\"\n [attr.aria-describedby]=\"\n nodeDescription(node) ? descriptionId(node.$path) : null\n \"\n [class]=\"cn('m-0 block w-full p-0', itemStateClass)\"\n (keydown.enter)=\"onTreeItemEnterKey(node, $event)\"\n (keydown.space)=\"onTreeItemSpaceKey(node, $event)\">\n <!-- 2. Ce div prend tout l'espace et intercepte tous les clics de la ligne. `ps-0.5`: the checkbox of a list option sits\n 2px further in than a tree's (the option has its own padding), so the checkboxes of the two facets, and the select-all of\n the bar of a popover, are on the same vertical line. -->\n <div\n class=\"flex w-full cursor-pointer items-center gap-2 py-0.5 ps-0.5 text-sm leading-6\"\n (click)=\"onTreeItemRowClick(node, $event)\"\n (mousedown)=\"$event.stopPropagation()\"\n (pointerdown)=\"$event.stopPropagation()\">\n @if (showCheckbox()) {\n <!-- `inert` keeps the native input out of the tab order and out of hit-testing:\n a role=\"treeitem\" admits no focusable descendant, and the click must reach\n the row handler above. Selection is announced by the <li>'s aria-selected. -->\n <!-- --spacing-xs: 0 kills the gap the checkbox keeps between its box and its\n (here empty) text block. Set as a custom property rather than a utility\n class: the CVA already sets gap-(--spacing-xs), and two same-specificity\n classes would be resolved by stylesheet order, not by us. -->\n <span\n inert\n aria-hidden=\"true\"\n class=\"inline-flex shrink-0\"\n style=\"--spacing-xs: 0\">\n <checkbox\n [checked]=\"treeItem.selected() || node.$selectedVisually || false\"\n [disabled]=\"node.disabled\"\n [class]=\"\n cn(\n 'pointer-events-none',\n (treeItem.selected() || node.$selectedVisually) &&\n 'selected-icon'\n )\n \" />\n </span>\n }\n <!-- Depth indent sits on the chevron, after the checkbox: the checkboxes of every\n level stay flush left (aligned with a flat list's), and since this adds no extra\n flex child, the step between consecutive levels is exactly one indent. -->\n @let depthIndent =\n \"calc(var(--agg-tree-indent, 0.5rem) * \" +\n (treeItem.level() - 1) +\n \")\";\n @if (node.hasChildren) {\n <!-- Expand/collapse affordance for the mouse only, hence a <span> and not a <button>:\n the keyboard already expands with ArrowRight / ArrowLeft, handled by ngTreeItem, and\n a role=\"treeitem\" admits no focusable descendant — so this was a <button> nobody\n could reach, already carrying aria-hidden and tabindex=\"-1\" to say so. Every audit\n reads it as a button with no accessible name (RGAA 7.1, 11.9). Same shape as the leaf\n branch below. -->\n <span\n aria-hidden=\"true\"\n (click)=\"toggleTreeNode(treeItem, $event)\"\n [style.marginInlineStart]=\"depthIndent\"\n class=\"inline-flex cursor-pointer items-center justify-center\">\n <ChevronRightIcon\n [class]=\"\n cn('expand-icon shrink-0', node.$opened && 'rotate-90')\n \" />\n <FolderIcon class=\"shrink-0\" />\n </span>\n } @else {\n <span\n aria-hidden=\"true\"\n [style.marginInlineStart]=\"depthIndent\"\n class=\"inline-flex items-center justify-center\">\n <!-- Reserves the chevron's own width (`.expand-icon` is a styling hook with\n no rule of its own), so a leaf lines up with its parent nodes. -->\n <span class=\"expand-icon w-4 shrink-0\"></span>\n <FileIcon class=\"size-4 shrink-0\" />\n </span>\n }\n @if (resolvedItemTemplate(); as tpl) {\n <span\n class=\"grow truncate select-none\"\n (click)=\"onTreeItemTextClick(node, $event)\">\n <ng-container\n [ngTemplateOutlet]=\"tpl\"\n [ngTemplateOutletContext]=\"{\n $implicit: node,\n field: aggregation()?.column,\n selected: treeItem.selected() ?? false,\n count: node.count,\n name: label,\n searchText: searchText(),\n level: node.$level ?? 0,\n expanded: node.$opened ?? false,\n hasChildren: node.hasChildren ?? false\n }\" />\n </span>\n } @else {\n <!-- ce span est essentiel pour le style -->\n @let appliesOnLabelClick = quickFilter() || applyOnSelect();\n <span\n [class]=\"\n cn(\n 'grow truncate select-none',\n appliesOnLabelClick && 'hover:underline'\n )\n \"\n [title]=\"\n appliesOnLabelClick\n ? ((isTreeItemFiltered(node)\n ? 'filters.removeFilter'\n : 'filters.addFilter'\n ) | transloco) +\n ': ' +\n label\n : label\n \"\n (click)=\"onTreeItemTextClick(node, $event)\">\n @for (\n chunk of label | highlightWord: searchText() : 10;\n track $index\n ) {\n <span [class]=\"{ 'font-bold': chunk.match }\" aria-hidden=\"true\">{{\n chunk.text\n }}</span>\n }\n </span>\n @if (showCount() && node.count > 0) {\n <span class=\"ml-auto px-1 text-xs\" aria-hidden=\"true\">{{\n node.count\n }}</span>\n }\n }\n @let description = nodeDescription(node);\n @if (description) {\n <!-- `hidden`, not `sr-only`: read through aria-describedby only. A visually-hidden\n span would also be counted in the treeitem's name (computed from its content). -->\n <span hidden [id]=\"descriptionId(node.$path)\">{{ description }}</span>\n }\n </div>\n </li>\n @if (node.hasChildren) {\n <!-- No indentation here: it would shift the checkbox too. Depth is applied\n inside the row, after the checkbox, so every checkbox stays flush left. -->\n <!-- The <ul> sits INSIDE the deferred content, not around it: `ngTreeItemGroup` only\n instantiates its view when the node opens, so a <ul> declared outside stayed in the DOM,\n empty, for every collapsed node — a structural tag with nothing to structure (RGAA 8.9).\n The @if cannot move onto the <ul> either: the directive registers itself with its tree\n item on init and has to exist for as long as the item does. -->\n <ng-template\n ngTreeItemGroup\n [ownedBy]=\"treeItem\"\n #group=\"ngTreeItemGroup\">\n <ul role=\"group\">\n <ng-template\n [ngTemplateOutlet]=\"treeNodes\"\n [ngTemplateOutletContext]=\"{ nodes: node.items, parent: group }\" />\n </ul>\n </ng-template>\n }\n }\n</ng-template>\n",
26137
+ template: "@if (!aggregation()?.isTree) {\n <div role=\"alert\" class=\"p-2 text-sm text-red-500\">\n <triangle-alert-icon class=\"mr-1 shrink-0\" />\n The aggregationTree component does not support list aggregations. Please use\n the &lt;Aggregation /&gt; component instead.\n </div>\n}\n<aggregation-panel\n [id]=\"id()\"\n [collapsible]=\"collapsible()\"\n [collapsed]=\"collapsed()\"\n [isEmpty]=\"isEmpty()\"\n [isDate]=\"isDate()\"\n [aggregation]=\"aggregation()\"\n [showFiltersCount]=\"showFiltersCount()\"\n [filtersCount]=\"filtersCount()\"\n [hasFilters]=\"hasFilters()\"\n [selection]=\"selection()\"\n [isAllSelected]=\"isAllSelected()\"\n [headless]=\"headless()\"\n [selectedCount]=\"selected().length\"\n [(searchText)]=\"searchText\"\n [itemsLength]=\"items().length\"\n [hasMore]=\"aggregation()?.$hasMore ?? false\"\n [searchedItemsLength]=\"searchedItems().length\"\n (cleared)=\"clear()\"\n (applied)=\"apply()\"\n (allSelected)=\"selectAll()\"\n (allUnselected)=\"unselectAll()\"\n (loadedMore)=\"loadMore()\">\n <!-- Relay the host's `label` projection into the panel's `label` slot. When the\n host projects nothing, the local <ng-content> falls back to the icon + display\n (mirrors AggregationPanel's own fallback), so the header is never empty. -->\n <ng-container ngProjectAs=\"label\">\n <ng-content select=\"label\">\n @let labelIcon = aggregation()?.icon;\n @if (labelIcon) {\n <FaIcon [faClass]=\"labelIcon\" class=\"mr-1 shrink-0\" />\n }\n <span class=\"grow truncate\">{{\n aggregation()?.display | syslang | transloco\n }}</span>\n </ng-content>\n </ng-container>\n <div class=\"min-h-0 w-full flex-1 scrollbar-thin overflow-auto pb-3\">\n <ul\n ngTree\n #tree=\"ngTree\"\n [multi]=\"true\"\n [(value)]=\"selected\"\n (valueChange)=\"onSelectionChange($event)\"\n /* Translated like the flat list's own label (aggregation-list.html): this was the raw\n `display`, so the accessible name of a whole facet came out in the source language.\n The pipes apply to the whole nullish chain, `|` being the loosest operator Angular\n parses — no parentheses needed. */\n [attr.aria-label]=\"aggregation()?.display ?? name() | syslang | transloco\"\n class=\"basic-tree m-1\">\n <ng-template\n [ngTemplateOutlet]=\"treeNodes\"\n [ngTemplateOutletContext]=\"{ nodes: nodes(), parent: tree }\" />\n </ul>\n </div>\n</aggregation-panel>\n\n<ng-template #treeNodes let-nodes=\"nodes\" let-parent=\"parent\">\n @for (node of nodes; track node.$path) {\n @let label = node.display ?? node.value ?? \"\";\n <li\n ngTreeItem\n #treeItem=\"ngTreeItem\"\n [parent]=\"parent\"\n [value]=\"node.$path\"\n [label]=\"label\"\n [disabled]=\"node.disabled\"\n [(expanded)]=\"node.$opened\"\n (expandedChange)=\"onExpandedChange(node, $event)\"\n [attr.aria-label]=\"label\"\n [attr.aria-describedby]=\"\n nodeDescription(node) ? descriptionId(node.$path) : null\n \"\n [class]=\"cn('m-0 block w-full p-0', itemStateClass)\"\n (keydown.enter)=\"onTreeItemEnterKey(node, $event)\"\n (keydown.space)=\"onTreeItemSpaceKey(node, $event)\">\n <!-- 2. Ce div prend tout l'espace et intercepte tous les clics de la ligne. `ps-0.5`: the checkbox of a list option sits\n 2px further in than a tree's (the option has its own padding), so the checkboxes of the two facets, and the select-all of\n the bar of a popover, are on the same vertical line. -->\n <div\n class=\"flex w-full cursor-pointer items-center gap-2 py-0.5 ps-0.5 text-sm leading-6\"\n (click)=\"onTreeItemRowClick(node, $event)\"\n (mousedown)=\"$event.stopPropagation()\"\n (pointerdown)=\"$event.stopPropagation()\">\n @if (showCheckbox()) {\n <!-- `inert` keeps the native input out of the tab order and out of hit-testing:\n a role=\"treeitem\" admits no focusable descendant, and the click must reach\n the row handler above. Selection is announced by the <li>'s aria-selected. -->\n <!-- --spacing-xs: 0 kills the gap the checkbox keeps between its box and its\n (here empty) text block. Set as a custom property rather than a utility\n class: the CVA already sets gap-(--spacing-xs), and two same-specificity\n classes would be resolved by stylesheet order, not by us. -->\n <span\n inert\n aria-hidden=\"true\"\n class=\"inline-flex shrink-0\"\n style=\"--spacing-xs: 0\">\n <checkbox\n [checked]=\"treeItem.selected() || node.$selectedVisually || false\"\n [disabled]=\"node.disabled\"\n [class]=\"\n cn(\n 'pointer-events-none',\n (treeItem.selected() || node.$selectedVisually) &&\n 'selected-icon'\n )\n \" />\n </span>\n }\n <!-- Depth indent sits on the chevron, after the checkbox: the checkboxes of every\n level stay flush left (aligned with a flat list's), and since this adds no extra\n flex child, the step between consecutive levels is exactly one indent. -->\n @let depthIndent =\n \"calc(var(--agg-tree-indent, 0.5rem) * \" +\n (treeItem.level() - 1) +\n \")\";\n @if (node.hasChildren) {\n <!-- Expand/collapse affordance for the mouse only, hence a <span> and not a <button>:\n the keyboard already expands with ArrowRight / ArrowLeft, handled by ngTreeItem, and\n a role=\"treeitem\" admits no focusable descendant — so this was a <button> nobody\n could reach, already carrying aria-hidden and tabindex=\"-1\" to say so. Every audit\n reads it as a button with no accessible name (RGAA 7.1, 11.9). Same shape as the leaf\n branch below. -->\n <span\n aria-hidden=\"true\"\n (click)=\"toggleTreeNode(treeItem, $event)\"\n [style.marginInlineStart]=\"depthIndent\"\n class=\"inline-flex cursor-pointer items-center justify-center\">\n <ChevronRightIcon\n [class]=\"\n cn('expand-icon shrink-0', node.$opened && 'rotate-90')\n \" />\n <FolderIcon class=\"shrink-0\" />\n </span>\n } @else {\n <span\n aria-hidden=\"true\"\n [style.marginInlineStart]=\"depthIndent\"\n class=\"inline-flex items-center justify-center\">\n <!-- Reserves the chevron's own width (`.expand-icon` is a styling hook with\n no rule of its own), so a leaf lines up with its parent nodes. -->\n <span class=\"expand-icon w-4 shrink-0\"></span>\n <FileIcon class=\"size-4 shrink-0\" />\n </span>\n }\n @if (resolvedItemTemplate(); as tpl) {\n <!-- Cut off, the label shows whole in a tooltip; a label that fits needs none. -->\n <span\n class=\"grow truncate select-none\"\n truncated\n #cutCustom=\"truncated\"\n [tooltip]=\"label\"\n [tooltipDisabled]=\"!cutCustom.isTruncated()\"\n (click)=\"onTreeItemTextClick(node, $event)\">\n <ng-container\n [ngTemplateOutlet]=\"tpl\"\n [ngTemplateOutletContext]=\"{\n $implicit: node,\n field: aggregation()?.column,\n selected: treeItem.selected() ?? false,\n count: node.count,\n name: label,\n searchText: searchText(),\n level: node.$level ?? 0,\n expanded: node.$opened ?? false,\n hasChildren: node.hasChildren ?? false\n }\" />\n </span>\n } @else {\n <!-- ce span est essentiel pour le style -->\n @let appliesOnLabelClick = quickFilter() || applyOnSelect();\n <!-- Cut off, the label shows whole in a tooltip (`truncated` measures it); a label that fits has none. -->\n <span\n [class]=\"\n cn(\n 'grow truncate select-none',\n appliesOnLabelClick && 'hover:underline'\n )\n \"\n truncated\n #cut=\"truncated\"\n [tooltip]=\"label\"\n [tooltipDisabled]=\"!cut.isTruncated()\"\n (click)=\"onTreeItemTextClick(node, $event)\">\n @for (\n chunk of label | highlightWord: searchText() : 10;\n track $index\n ) {\n <span [class]=\"{ 'font-bold': chunk.match }\" aria-hidden=\"true\">{{\n chunk.text\n }}</span>\n }\n </span>\n @if (showCount() && node.count > 0) {\n <span class=\"ml-auto px-1 text-xs\" aria-hidden=\"true\">{{\n node.count\n }}</span>\n }\n }\n @let description = nodeDescription(node);\n @if (description) {\n <!-- `hidden`, not `sr-only`: read through aria-describedby only. A visually-hidden\n span would also be counted in the treeitem's name (computed from its content). -->\n <span hidden [id]=\"descriptionId(node.$path)\">{{ description }}</span>\n }\n </div>\n </li>\n @if (node.hasChildren) {\n <!-- No indentation here: it would shift the checkbox too. Depth is applied\n inside the row, after the checkbox, so every checkbox stays flush left. -->\n <!-- The <ul> sits INSIDE the deferred content, not around it: `ngTreeItemGroup` only\n instantiates its view when the node opens, so a <ul> declared outside stayed in the DOM,\n empty, for every collapsed node — a structural tag with nothing to structure (RGAA 8.9).\n The @if cannot move onto the <ul> either: the directive registers itself with its tree\n item on init and has to exist for as long as the item does. -->\n <ng-template\n ngTreeItemGroup\n [ownedBy]=\"treeItem\"\n #group=\"ngTreeItemGroup\">\n <ul role=\"group\">\n <ng-template\n [ngTemplateOutlet]=\"treeNodes\"\n [ngTemplateOutletContext]=\"{ nodes: node.items, parent: group }\" />\n </ul>\n </ng-template>\n }\n }\n</ng-template>\n",
26042
26138
  dependencies: [
26043
26139
  {
26044
26140
  kind: "directive",
@@ -26088,12 +26184,45 @@ var AggregationTreeComponent = class AggregationTreeComponent {
26088
26184
  selector: "folder, folder-icon, Folder, FolderIcon",
26089
26185
  inputs: ["class"]
26090
26186
  },
26187
+ {
26188
+ kind: "directive",
26189
+ type: TooltipDirective,
26190
+ selector: "[tooltip]",
26191
+ inputs: [
26192
+ "tooltip",
26193
+ "tooltipPosition",
26194
+ "tooltipEvent",
26195
+ "showDelay",
26196
+ "hideDelay",
26197
+ "life",
26198
+ "tooltipOffset",
26199
+ "autoHide",
26200
+ "hideOnEscape",
26201
+ "escape",
26202
+ "tooltipDisabled",
26203
+ "tooltipArrow",
26204
+ "tooltipStyleClass",
26205
+ "tooltipOptions",
26206
+ "tooltip-position",
26207
+ "tooltip-delay",
26208
+ "tooltip-duration",
26209
+ "tooltip-offset"
26210
+ ],
26211
+ exportAs: ["tooltip"]
26212
+ },
26091
26213
  {
26092
26214
  kind: "component",
26093
26215
  type: TriangleAlertIcon,
26094
26216
  selector: "triangle-alert, triangle-alert-icon, TriangleAlert, TriangleAlertIcon",
26095
26217
  inputs: ["class"]
26096
26218
  },
26219
+ {
26220
+ kind: "directive",
26221
+ type: TruncatedDirective,
26222
+ selector: "[truncated]",
26223
+ inputs: ["truncated"],
26224
+ exportAs: ["truncated"]
26225
+ },
26097
26226
  {
26098
26227
  kind: "directive",
26099
26228
  type: Tree,
@@ -26205,7 +26334,9 @@ i0.ɵɵngDeclareClassMetadata({
26205
26334
  FaIconComponent,
26206
26335
  FileIcon,
26207
26336
  FolderIcon,
26337
+ TooltipDirective,
26208
26338
  TriangleAlertIcon,
26339
+ TruncatedDirective,
26209
26340
  HighlightWordPipe,
26210
26341
  SyslangPipe,
26211
26342
  TranslocoPipe,
@@ -26215,7 +26346,7 @@ i0.ɵɵngDeclareClassMetadata({
26215
26346
  AggregationPanelComponent
26216
26347
  ],
26217
26348
  host: { "[class]": "cn(\"flex h-[inherit] max-h-[inherit] flex-col\", class(), (isMissing() || (hideWhenEmpty() && isHidden())) && \"hidden\")" },
26218
- template: "@if (!aggregation()?.isTree) {\n <div role=\"alert\" class=\"p-2 text-sm text-red-500\">\n <triangle-alert-icon class=\"mr-1 shrink-0\" />\n The aggregationTree component does not support list aggregations. Please use\n the &lt;Aggregation /&gt; component instead.\n </div>\n}\n<aggregation-panel\n [id]=\"id()\"\n [collapsible]=\"collapsible()\"\n [collapsed]=\"collapsed()\"\n [isEmpty]=\"isEmpty()\"\n [isDate]=\"isDate()\"\n [aggregation]=\"aggregation()\"\n [showFiltersCount]=\"showFiltersCount()\"\n [filtersCount]=\"filtersCount()\"\n [hasFilters]=\"hasFilters()\"\n [selection]=\"selection()\"\n [isAllSelected]=\"isAllSelected()\"\n [headless]=\"headless()\"\n [selectedCount]=\"selected().length\"\n [(searchText)]=\"searchText\"\n [itemsLength]=\"items().length\"\n [hasMore]=\"aggregation()?.$hasMore ?? false\"\n [searchedItemsLength]=\"searchedItems().length\"\n (cleared)=\"clear()\"\n (applied)=\"apply()\"\n (allSelected)=\"selectAll()\"\n (allUnselected)=\"unselectAll()\"\n (loadedMore)=\"loadMore()\">\n <!-- Relay the host's `label` projection into the panel's `label` slot. When the\n host projects nothing, the local <ng-content> falls back to the icon + display\n (mirrors AggregationPanel's own fallback), so the header is never empty. -->\n <ng-container ngProjectAs=\"label\">\n <ng-content select=\"label\">\n @let labelIcon = aggregation()?.icon;\n @if (labelIcon) {\n <FaIcon [faClass]=\"labelIcon\" class=\"mr-1 shrink-0\" />\n }\n <span class=\"grow truncate\">{{\n aggregation()?.display | syslang | transloco\n }}</span>\n </ng-content>\n </ng-container>\n <div class=\"min-h-0 w-full flex-1 scrollbar-thin overflow-auto pb-3\">\n <ul\n ngTree\n #tree=\"ngTree\"\n [multi]=\"true\"\n [(value)]=\"selected\"\n (valueChange)=\"onSelectionChange($event)\"\n /* Translated like the flat list's own label (aggregation-list.html): this was the raw\n `display`, so the accessible name of a whole facet came out in the source language.\n The pipes apply to the whole nullish chain, `|` being the loosest operator Angular\n parses — no parentheses needed. */\n [attr.aria-label]=\"aggregation()?.display ?? name() | syslang | transloco\"\n class=\"basic-tree m-1\">\n <ng-template\n [ngTemplateOutlet]=\"treeNodes\"\n [ngTemplateOutletContext]=\"{ nodes: nodes(), parent: tree }\" />\n </ul>\n </div>\n</aggregation-panel>\n\n<ng-template #treeNodes let-nodes=\"nodes\" let-parent=\"parent\">\n @for (node of nodes; track node.$path) {\n @let label = node.display ?? node.value ?? \"\";\n <li\n ngTreeItem\n #treeItem=\"ngTreeItem\"\n [parent]=\"parent\"\n [value]=\"node.$path\"\n [label]=\"label\"\n [disabled]=\"node.disabled\"\n [(expanded)]=\"node.$opened\"\n (expandedChange)=\"onExpandedChange(node, $event)\"\n [attr.aria-label]=\"label\"\n [attr.aria-describedby]=\"\n nodeDescription(node) ? descriptionId(node.$path) : null\n \"\n [class]=\"cn('m-0 block w-full p-0', itemStateClass)\"\n (keydown.enter)=\"onTreeItemEnterKey(node, $event)\"\n (keydown.space)=\"onTreeItemSpaceKey(node, $event)\">\n <!-- 2. Ce div prend tout l'espace et intercepte tous les clics de la ligne. `ps-0.5`: the checkbox of a list option sits\n 2px further in than a tree's (the option has its own padding), so the checkboxes of the two facets, and the select-all of\n the bar of a popover, are on the same vertical line. -->\n <div\n class=\"flex w-full cursor-pointer items-center gap-2 py-0.5 ps-0.5 text-sm leading-6\"\n (click)=\"onTreeItemRowClick(node, $event)\"\n (mousedown)=\"$event.stopPropagation()\"\n (pointerdown)=\"$event.stopPropagation()\">\n @if (showCheckbox()) {\n <!-- `inert` keeps the native input out of the tab order and out of hit-testing:\n a role=\"treeitem\" admits no focusable descendant, and the click must reach\n the row handler above. Selection is announced by the <li>'s aria-selected. -->\n <!-- --spacing-xs: 0 kills the gap the checkbox keeps between its box and its\n (here empty) text block. Set as a custom property rather than a utility\n class: the CVA already sets gap-(--spacing-xs), and two same-specificity\n classes would be resolved by stylesheet order, not by us. -->\n <span\n inert\n aria-hidden=\"true\"\n class=\"inline-flex shrink-0\"\n style=\"--spacing-xs: 0\">\n <checkbox\n [checked]=\"treeItem.selected() || node.$selectedVisually || false\"\n [disabled]=\"node.disabled\"\n [class]=\"\n cn(\n 'pointer-events-none',\n (treeItem.selected() || node.$selectedVisually) &&\n 'selected-icon'\n )\n \" />\n </span>\n }\n <!-- Depth indent sits on the chevron, after the checkbox: the checkboxes of every\n level stay flush left (aligned with a flat list's), and since this adds no extra\n flex child, the step between consecutive levels is exactly one indent. -->\n @let depthIndent =\n \"calc(var(--agg-tree-indent, 0.5rem) * \" +\n (treeItem.level() - 1) +\n \")\";\n @if (node.hasChildren) {\n <!-- Expand/collapse affordance for the mouse only, hence a <span> and not a <button>:\n the keyboard already expands with ArrowRight / ArrowLeft, handled by ngTreeItem, and\n a role=\"treeitem\" admits no focusable descendant — so this was a <button> nobody\n could reach, already carrying aria-hidden and tabindex=\"-1\" to say so. Every audit\n reads it as a button with no accessible name (RGAA 7.1, 11.9). Same shape as the leaf\n branch below. -->\n <span\n aria-hidden=\"true\"\n (click)=\"toggleTreeNode(treeItem, $event)\"\n [style.marginInlineStart]=\"depthIndent\"\n class=\"inline-flex cursor-pointer items-center justify-center\">\n <ChevronRightIcon\n [class]=\"\n cn('expand-icon shrink-0', node.$opened && 'rotate-90')\n \" />\n <FolderIcon class=\"shrink-0\" />\n </span>\n } @else {\n <span\n aria-hidden=\"true\"\n [style.marginInlineStart]=\"depthIndent\"\n class=\"inline-flex items-center justify-center\">\n <!-- Reserves the chevron's own width (`.expand-icon` is a styling hook with\n no rule of its own), so a leaf lines up with its parent nodes. -->\n <span class=\"expand-icon w-4 shrink-0\"></span>\n <FileIcon class=\"size-4 shrink-0\" />\n </span>\n }\n @if (resolvedItemTemplate(); as tpl) {\n <span\n class=\"grow truncate select-none\"\n (click)=\"onTreeItemTextClick(node, $event)\">\n <ng-container\n [ngTemplateOutlet]=\"tpl\"\n [ngTemplateOutletContext]=\"{\n $implicit: node,\n field: aggregation()?.column,\n selected: treeItem.selected() ?? false,\n count: node.count,\n name: label,\n searchText: searchText(),\n level: node.$level ?? 0,\n expanded: node.$opened ?? false,\n hasChildren: node.hasChildren ?? false\n }\" />\n </span>\n } @else {\n <!-- ce span est essentiel pour le style -->\n @let appliesOnLabelClick = quickFilter() || applyOnSelect();\n <span\n [class]=\"\n cn(\n 'grow truncate select-none',\n appliesOnLabelClick && 'hover:underline'\n )\n \"\n [title]=\"\n appliesOnLabelClick\n ? ((isTreeItemFiltered(node)\n ? 'filters.removeFilter'\n : 'filters.addFilter'\n ) | transloco) +\n ': ' +\n label\n : label\n \"\n (click)=\"onTreeItemTextClick(node, $event)\">\n @for (\n chunk of label | highlightWord: searchText() : 10;\n track $index\n ) {\n <span [class]=\"{ 'font-bold': chunk.match }\" aria-hidden=\"true\">{{\n chunk.text\n }}</span>\n }\n </span>\n @if (showCount() && node.count > 0) {\n <span class=\"ml-auto px-1 text-xs\" aria-hidden=\"true\">{{\n node.count\n }}</span>\n }\n }\n @let description = nodeDescription(node);\n @if (description) {\n <!-- `hidden`, not `sr-only`: read through aria-describedby only. A visually-hidden\n span would also be counted in the treeitem's name (computed from its content). -->\n <span hidden [id]=\"descriptionId(node.$path)\">{{ description }}</span>\n }\n </div>\n </li>\n @if (node.hasChildren) {\n <!-- No indentation here: it would shift the checkbox too. Depth is applied\n inside the row, after the checkbox, so every checkbox stays flush left. -->\n <!-- The <ul> sits INSIDE the deferred content, not around it: `ngTreeItemGroup` only\n instantiates its view when the node opens, so a <ul> declared outside stayed in the DOM,\n empty, for every collapsed node — a structural tag with nothing to structure (RGAA 8.9).\n The @if cannot move onto the <ul> either: the directive registers itself with its tree\n item on init and has to exist for as long as the item does. -->\n <ng-template\n ngTreeItemGroup\n [ownedBy]=\"treeItem\"\n #group=\"ngTreeItemGroup\">\n <ul role=\"group\">\n <ng-template\n [ngTemplateOutlet]=\"treeNodes\"\n [ngTemplateOutletContext]=\"{ nodes: node.items, parent: group }\" />\n </ul>\n </ng-template>\n }\n }\n</ng-template>\n"
26349
+ template: "@if (!aggregation()?.isTree) {\n <div role=\"alert\" class=\"p-2 text-sm text-red-500\">\n <triangle-alert-icon class=\"mr-1 shrink-0\" />\n The aggregationTree component does not support list aggregations. Please use\n the &lt;Aggregation /&gt; component instead.\n </div>\n}\n<aggregation-panel\n [id]=\"id()\"\n [collapsible]=\"collapsible()\"\n [collapsed]=\"collapsed()\"\n [isEmpty]=\"isEmpty()\"\n [isDate]=\"isDate()\"\n [aggregation]=\"aggregation()\"\n [showFiltersCount]=\"showFiltersCount()\"\n [filtersCount]=\"filtersCount()\"\n [hasFilters]=\"hasFilters()\"\n [selection]=\"selection()\"\n [isAllSelected]=\"isAllSelected()\"\n [headless]=\"headless()\"\n [selectedCount]=\"selected().length\"\n [(searchText)]=\"searchText\"\n [itemsLength]=\"items().length\"\n [hasMore]=\"aggregation()?.$hasMore ?? false\"\n [searchedItemsLength]=\"searchedItems().length\"\n (cleared)=\"clear()\"\n (applied)=\"apply()\"\n (allSelected)=\"selectAll()\"\n (allUnselected)=\"unselectAll()\"\n (loadedMore)=\"loadMore()\">\n <!-- Relay the host's `label` projection into the panel's `label` slot. When the\n host projects nothing, the local <ng-content> falls back to the icon + display\n (mirrors AggregationPanel's own fallback), so the header is never empty. -->\n <ng-container ngProjectAs=\"label\">\n <ng-content select=\"label\">\n @let labelIcon = aggregation()?.icon;\n @if (labelIcon) {\n <FaIcon [faClass]=\"labelIcon\" class=\"mr-1 shrink-0\" />\n }\n <span class=\"grow truncate\">{{\n aggregation()?.display | syslang | transloco\n }}</span>\n </ng-content>\n </ng-container>\n <div class=\"min-h-0 w-full flex-1 scrollbar-thin overflow-auto pb-3\">\n <ul\n ngTree\n #tree=\"ngTree\"\n [multi]=\"true\"\n [(value)]=\"selected\"\n (valueChange)=\"onSelectionChange($event)\"\n /* Translated like the flat list's own label (aggregation-list.html): this was the raw\n `display`, so the accessible name of a whole facet came out in the source language.\n The pipes apply to the whole nullish chain, `|` being the loosest operator Angular\n parses — no parentheses needed. */\n [attr.aria-label]=\"aggregation()?.display ?? name() | syslang | transloco\"\n class=\"basic-tree m-1\">\n <ng-template\n [ngTemplateOutlet]=\"treeNodes\"\n [ngTemplateOutletContext]=\"{ nodes: nodes(), parent: tree }\" />\n </ul>\n </div>\n</aggregation-panel>\n\n<ng-template #treeNodes let-nodes=\"nodes\" let-parent=\"parent\">\n @for (node of nodes; track node.$path) {\n @let label = node.display ?? node.value ?? \"\";\n <li\n ngTreeItem\n #treeItem=\"ngTreeItem\"\n [parent]=\"parent\"\n [value]=\"node.$path\"\n [label]=\"label\"\n [disabled]=\"node.disabled\"\n [(expanded)]=\"node.$opened\"\n (expandedChange)=\"onExpandedChange(node, $event)\"\n [attr.aria-label]=\"label\"\n [attr.aria-describedby]=\"\n nodeDescription(node) ? descriptionId(node.$path) : null\n \"\n [class]=\"cn('m-0 block w-full p-0', itemStateClass)\"\n (keydown.enter)=\"onTreeItemEnterKey(node, $event)\"\n (keydown.space)=\"onTreeItemSpaceKey(node, $event)\">\n <!-- 2. Ce div prend tout l'espace et intercepte tous les clics de la ligne. `ps-0.5`: the checkbox of a list option sits\n 2px further in than a tree's (the option has its own padding), so the checkboxes of the two facets, and the select-all of\n the bar of a popover, are on the same vertical line. -->\n <div\n class=\"flex w-full cursor-pointer items-center gap-2 py-0.5 ps-0.5 text-sm leading-6\"\n (click)=\"onTreeItemRowClick(node, $event)\"\n (mousedown)=\"$event.stopPropagation()\"\n (pointerdown)=\"$event.stopPropagation()\">\n @if (showCheckbox()) {\n <!-- `inert` keeps the native input out of the tab order and out of hit-testing:\n a role=\"treeitem\" admits no focusable descendant, and the click must reach\n the row handler above. Selection is announced by the <li>'s aria-selected. -->\n <!-- --spacing-xs: 0 kills the gap the checkbox keeps between its box and its\n (here empty) text block. Set as a custom property rather than a utility\n class: the CVA already sets gap-(--spacing-xs), and two same-specificity\n classes would be resolved by stylesheet order, not by us. -->\n <span\n inert\n aria-hidden=\"true\"\n class=\"inline-flex shrink-0\"\n style=\"--spacing-xs: 0\">\n <checkbox\n [checked]=\"treeItem.selected() || node.$selectedVisually || false\"\n [disabled]=\"node.disabled\"\n [class]=\"\n cn(\n 'pointer-events-none',\n (treeItem.selected() || node.$selectedVisually) &&\n 'selected-icon'\n )\n \" />\n </span>\n }\n <!-- Depth indent sits on the chevron, after the checkbox: the checkboxes of every\n level stay flush left (aligned with a flat list's), and since this adds no extra\n flex child, the step between consecutive levels is exactly one indent. -->\n @let depthIndent =\n \"calc(var(--agg-tree-indent, 0.5rem) * \" +\n (treeItem.level() - 1) +\n \")\";\n @if (node.hasChildren) {\n <!-- Expand/collapse affordance for the mouse only, hence a <span> and not a <button>:\n the keyboard already expands with ArrowRight / ArrowLeft, handled by ngTreeItem, and\n a role=\"treeitem\" admits no focusable descendant — so this was a <button> nobody\n could reach, already carrying aria-hidden and tabindex=\"-1\" to say so. Every audit\n reads it as a button with no accessible name (RGAA 7.1, 11.9). Same shape as the leaf\n branch below. -->\n <span\n aria-hidden=\"true\"\n (click)=\"toggleTreeNode(treeItem, $event)\"\n [style.marginInlineStart]=\"depthIndent\"\n class=\"inline-flex cursor-pointer items-center justify-center\">\n <ChevronRightIcon\n [class]=\"\n cn('expand-icon shrink-0', node.$opened && 'rotate-90')\n \" />\n <FolderIcon class=\"shrink-0\" />\n </span>\n } @else {\n <span\n aria-hidden=\"true\"\n [style.marginInlineStart]=\"depthIndent\"\n class=\"inline-flex items-center justify-center\">\n <!-- Reserves the chevron's own width (`.expand-icon` is a styling hook with\n no rule of its own), so a leaf lines up with its parent nodes. -->\n <span class=\"expand-icon w-4 shrink-0\"></span>\n <FileIcon class=\"size-4 shrink-0\" />\n </span>\n }\n @if (resolvedItemTemplate(); as tpl) {\n <!-- Cut off, the label shows whole in a tooltip; a label that fits needs none. -->\n <span\n class=\"grow truncate select-none\"\n truncated\n #cutCustom=\"truncated\"\n [tooltip]=\"label\"\n [tooltipDisabled]=\"!cutCustom.isTruncated()\"\n (click)=\"onTreeItemTextClick(node, $event)\">\n <ng-container\n [ngTemplateOutlet]=\"tpl\"\n [ngTemplateOutletContext]=\"{\n $implicit: node,\n field: aggregation()?.column,\n selected: treeItem.selected() ?? false,\n count: node.count,\n name: label,\n searchText: searchText(),\n level: node.$level ?? 0,\n expanded: node.$opened ?? false,\n hasChildren: node.hasChildren ?? false\n }\" />\n </span>\n } @else {\n <!-- ce span est essentiel pour le style -->\n @let appliesOnLabelClick = quickFilter() || applyOnSelect();\n <!-- Cut off, the label shows whole in a tooltip (`truncated` measures it); a label that fits has none. -->\n <span\n [class]=\"\n cn(\n 'grow truncate select-none',\n appliesOnLabelClick && 'hover:underline'\n )\n \"\n truncated\n #cut=\"truncated\"\n [tooltip]=\"label\"\n [tooltipDisabled]=\"!cut.isTruncated()\"\n (click)=\"onTreeItemTextClick(node, $event)\">\n @for (\n chunk of label | highlightWord: searchText() : 10;\n track $index\n ) {\n <span [class]=\"{ 'font-bold': chunk.match }\" aria-hidden=\"true\">{{\n chunk.text\n }}</span>\n }\n </span>\n @if (showCount() && node.count > 0) {\n <span class=\"ml-auto px-1 text-xs\" aria-hidden=\"true\">{{\n node.count\n }}</span>\n }\n }\n @let description = nodeDescription(node);\n @if (description) {\n <!-- `hidden`, not `sr-only`: read through aria-describedby only. A visually-hidden\n span would also be counted in the treeitem's name (computed from its content). -->\n <span hidden [id]=\"descriptionId(node.$path)\">{{ description }}</span>\n }\n </div>\n </li>\n @if (node.hasChildren) {\n <!-- No indentation here: it would shift the checkbox too. Depth is applied\n inside the row, after the checkbox, so every checkbox stays flush left. -->\n <!-- The <ul> sits INSIDE the deferred content, not around it: `ngTreeItemGroup` only\n instantiates its view when the node opens, so a <ul> declared outside stayed in the DOM,\n empty, for every collapsed node — a structural tag with nothing to structure (RGAA 8.9).\n The @if cannot move onto the <ul> either: the directive registers itself with its tree\n item on init and has to exist for as long as the item does. -->\n <ng-template\n ngTreeItemGroup\n [ownedBy]=\"treeItem\"\n #group=\"ngTreeItemGroup\">\n <ul role=\"group\">\n <ng-template\n [ngTemplateOutlet]=\"treeNodes\"\n [ngTemplateOutletContext]=\"{ nodes: node.items, parent: group }\" />\n </ul>\n </ng-template>\n }\n }\n</ng-template>\n"
26219
26350
  }]
26220
26351
  }],
26221
26352
  ctorParameters: () => [],
@@ -26923,7 +27054,7 @@ var AggregationListComponent = class AggregationListComponent {
26923
27054
  }
26924
27055
  ],
26925
27056
  ngImport: i0,
26926
- template: "@if (aggregation()?.isTree) {\n <div class=\"p-2 text-sm text-red-500\">\n <triangle-alert-icon class=\"mr-1 shrink-0\" />\n The aggregation component no longer supports tree aggregations. Please use\n the &lt;AggregationTree /&gt; component instead.\n </div>\n}\n<aggregation-panel\n [id]=\"id()\"\n [collapsible]=\"collapsible()\"\n [collapsed]=\"collapsed()\"\n [isEmpty]=\"isEmpty()\"\n [isDate]=\"isDate()\"\n [aggregation]=\"aggregation()\"\n [showFiltersCount]=\"showFiltersCount()\"\n [filtersCount]=\"filtersCount()\"\n [hasFilters]=\"hasFilters()\"\n [selection]=\"selection()\"\n [isAllSelected]=\"isAllSelected()\"\n [headless]=\"headless()\"\n [selectedCount]=\"selectedValues().length\"\n [(searchText)]=\"searchText\"\n [itemsLength]=\"items().length\"\n [hasMore]=\"aggregation()?.$hasMore ?? false\"\n [searchedItemsLength]=\"searchedItems().length\"\n (cleared)=\"clear()\"\n (applied)=\"apply()\"\n (allSelected)=\"selectAll()\"\n (allUnselected)=\"unselectAll()\"\n (loadedMore)=\"loadMore()\">\n <!-- Relay the host's `label` projection into the panel's `label` slot. When the\n host projects nothing, the local <ng-content> falls back to the icon + display\n (mirrors AggregationPanel's own fallback), so the header is never empty. -->\n <ng-container ngProjectAs=\"label\">\n <ng-content select=\"label\">\n @let labelIcon = aggregation()?.icon;\n @if (labelIcon) {\n <FaIcon [faClass]=\"labelIcon\" class=\"mr-1 shrink-0\" />\n }\n <span class=\"grow truncate\">{{\n aggregation()?.display | syslang | transloco\n }}</span>\n </ng-content>\n </ng-container>\n <div\n #scrollElement\n class=\"min-h-0 w-full flex-1 scrollbar-thin overflow-auto px-1 pt-1 pb-3\">\n <div\n ngListbox\n multi\n selectionMode=\"explicit\"\n [value]=\"selectedValues()\"\n (valueChange)=\"syncListboxValue($event)\"\n [wrap]=\"false\"\n [softDisabled]=\"false\"\n class=\"relative w-full\"\n [style.height]=\"virtualizer.getTotalSize() + 'px'\"\n role=\"listbox\"\n aria-multiselectable=\"true\"\n [attr.aria-label]=\"aggregation()?.display | syslang | transloco\">\n @for (vItem of virtualizer.getVirtualItems(); track vItem.index) {\n @let item = items()[vItem.index];\n @let description = rowDescription(item);\n <div\n #row\n ngOption\n [attr.aria-describedby]=\"\n description ? descriptionId(vItem.index) : null\n \"\n [attr.data-index]=\"vItem.index\"\n [value]=\"item.value\"\n [disabled]=\"item.count === 0\"\n [class]=\"cn('absolute w-full', menuItemBaseClass)\"\n (keydown.enter)=\"onOptionEnterKey(item, $event)\"\n (keydown.space)=\"\n onListItemRowClick(item, $event); $event.preventDefault()\n \"\n [style.transform]=\"'translateY(' + vItem.start + 'px)'\"\n role=\"option\">\n <ng-container\n [ngTemplateOutlet]=\"listItemTpl\"\n [ngTemplateOutletContext]=\"{\n $implicit: item,\n field: aggregation()?.column\n }\">\n </ng-container>\n @if (description) {\n <!-- `hidden`, not `sr-only`: read through aria-describedby only. A visually-hidden\n span would also be counted in the option's name (computed from its content). -->\n <span hidden [id]=\"descriptionId(vItem.index)\">{{\n description\n }}</span>\n }\n </div>\n }\n </div>\n </div>\n</aggregation-panel>\n\n<ng-template #listItemTpl let-item let-field=\"field\">\n <a\n [attr.aria-label]=\"listItemName(item) | syslang\"\n [class]=\"\n cn(\n '-m-1.5 flex min-w-0 grow items-center gap-2 leading-6',\n item.count === 0 && 'pointer-events-none text-(--font-neutral-muted)'\n )\n \"\n (click)=\"onListItemRowClick(item, $event)\">\n @if (showCheckbox()) {\n <!-- `inert` keeps the native input out of the tab order and out of hit-testing:\n keyboard navigation belongs to ngListbox/ngOption (roving tabindex), the\n selection is already exposed by role=\"option\" + aria-selected, and the click\n must reach the <a> above. Same treatment as the tree.\n --spacing-xs: 0 kills the gap the checkbox keeps between its box and its\n (here empty) text block. -->\n <span\n inert\n aria-hidden=\"true\"\n class=\"inline-flex shrink-0\"\n style=\"--spacing-xs: 0\">\n <checkbox\n [checked]=\"item.$selected ?? false\"\n [disabled]=\"item.count === 0\"\n [class]=\"'pointer-events-none'\" />\n </span>\n }\n @if (resolvedItemTemplate(); as tpl) {\n <ng-container\n [ngTemplateOutlet]=\"tpl\"\n [ngTemplateOutletContext]=\"{\n $implicit: item,\n field: field,\n selected: item.$selected ?? false,\n count: item.count,\n name: (listItemName(item) | syslang) ?? '',\n searchText: searchText()\n }\" />\n } @else {\n @let icon = $any(item).icon;\n @if (icon) {\n <FaIcon\n [faClass]=\"icon\"\n class=\"shrink-0 self-center justify-self-center\" />\n }\n @let appliesOnLabelClick = quickFilter() || applyOnSelect();\n <span\n [class]=\"\n cn(\n 'line-clamp-1 cursor-pointer break-all text-ellipsis select-none',\n appliesOnLabelClick && 'hover:underline'\n )\n \"\n [title]=\"\n appliesOnLabelClick\n ? ((isListItemFiltered(item, field)\n ? 'filters.removeFilter'\n : 'filters.addFilter'\n ) | transloco) +\n ': ' +\n (listItemName(item) | syslang)\n : (listItemName(item) | syslang)\n \"\n (click)=\"onListItemTextClick(item, $event)\">\n @for (\n chunk of (listItemName(item) | syslang) ?? \"\"\n | highlightWord: searchText() : 10;\n track $index\n ) {\n <span [class]=\"{ 'font-bold': chunk.match }\" aria-hidden=\"true\">{{\n chunk.text\n }}</span>\n }\n </span>\n @if (showCount() && item.count > 0) {\n <span class=\"ml-auto px-1 text-xs empty:hidden\" aria-hidden=\"true\">{{\n item.count\n }}</span>\n }\n }\n </a>\n</ng-template>\n",
27057
+ template: "@if (aggregation()?.isTree) {\n <div class=\"p-2 text-sm text-red-500\">\n <triangle-alert-icon class=\"mr-1 shrink-0\" />\n The aggregation component no longer supports tree aggregations. Please use\n the &lt;AggregationTree /&gt; component instead.\n </div>\n}\n<aggregation-panel\n [id]=\"id()\"\n [collapsible]=\"collapsible()\"\n [collapsed]=\"collapsed()\"\n [isEmpty]=\"isEmpty()\"\n [isDate]=\"isDate()\"\n [aggregation]=\"aggregation()\"\n [showFiltersCount]=\"showFiltersCount()\"\n [filtersCount]=\"filtersCount()\"\n [hasFilters]=\"hasFilters()\"\n [selection]=\"selection()\"\n [isAllSelected]=\"isAllSelected()\"\n [headless]=\"headless()\"\n [selectedCount]=\"selectedValues().length\"\n [(searchText)]=\"searchText\"\n [itemsLength]=\"items().length\"\n [hasMore]=\"aggregation()?.$hasMore ?? false\"\n [searchedItemsLength]=\"searchedItems().length\"\n (cleared)=\"clear()\"\n (applied)=\"apply()\"\n (allSelected)=\"selectAll()\"\n (allUnselected)=\"unselectAll()\"\n (loadedMore)=\"loadMore()\">\n <!-- Relay the host's `label` projection into the panel's `label` slot. When the\n host projects nothing, the local <ng-content> falls back to the icon + display\n (mirrors AggregationPanel's own fallback), so the header is never empty. -->\n <ng-container ngProjectAs=\"label\">\n <ng-content select=\"label\">\n @let labelIcon = aggregation()?.icon;\n @if (labelIcon) {\n <FaIcon [faClass]=\"labelIcon\" class=\"mr-1 shrink-0\" />\n }\n <span class=\"grow truncate\">{{\n aggregation()?.display | syslang | transloco\n }}</span>\n </ng-content>\n </ng-container>\n <div\n #scrollElement\n class=\"min-h-0 w-full flex-1 scrollbar-thin overflow-auto px-1 pt-1 pb-3\">\n <div\n ngListbox\n multi\n selectionMode=\"explicit\"\n [value]=\"selectedValues()\"\n (valueChange)=\"syncListboxValue($event)\"\n [wrap]=\"false\"\n [softDisabled]=\"false\"\n class=\"relative w-full\"\n [style.height]=\"virtualizer.getTotalSize() + 'px'\"\n role=\"listbox\"\n aria-multiselectable=\"true\"\n [attr.aria-label]=\"aggregation()?.display | syslang | transloco\">\n @for (vItem of virtualizer.getVirtualItems(); track vItem.index) {\n @let item = items()[vItem.index];\n @let description = rowDescription(item);\n <div\n #row\n ngOption\n [attr.aria-describedby]=\"\n description ? descriptionId(vItem.index) : null\n \"\n [attr.data-index]=\"vItem.index\"\n [value]=\"item.value\"\n [disabled]=\"item.count === 0\"\n [class]=\"cn('absolute w-full', menuItemBaseClass)\"\n (keydown.enter)=\"onOptionEnterKey(item, $event)\"\n (keydown.space)=\"\n onListItemRowClick(item, $event); $event.preventDefault()\n \"\n [style.transform]=\"'translateY(' + vItem.start + 'px)'\"\n role=\"option\">\n <ng-container\n [ngTemplateOutlet]=\"listItemTpl\"\n [ngTemplateOutletContext]=\"{\n $implicit: item,\n field: aggregation()?.column\n }\">\n </ng-container>\n @if (description) {\n <!-- `hidden`, not `sr-only`: read through aria-describedby only. A visually-hidden\n span would also be counted in the option's name (computed from its content). -->\n <span hidden [id]=\"descriptionId(vItem.index)\">{{\n description\n }}</span>\n }\n </div>\n }\n </div>\n </div>\n</aggregation-panel>\n\n<ng-template #listItemTpl let-item let-field=\"field\">\n <a\n [attr.aria-label]=\"listItemName(item) | syslang\"\n [class]=\"\n cn(\n '-m-1.5 flex min-w-0 grow items-center gap-2 leading-6',\n item.count === 0 && 'pointer-events-none text-(--font-neutral-muted)'\n )\n \"\n (click)=\"onListItemRowClick(item, $event)\">\n @if (showCheckbox()) {\n <!-- `inert` keeps the native input out of the tab order and out of hit-testing:\n keyboard navigation belongs to ngListbox/ngOption (roving tabindex), the\n selection is already exposed by role=\"option\" + aria-selected, and the click\n must reach the <a> above. Same treatment as the tree.\n --spacing-xs: 0 kills the gap the checkbox keeps between its box and its\n (here empty) text block. -->\n <span\n inert\n aria-hidden=\"true\"\n class=\"inline-flex shrink-0\"\n style=\"--spacing-xs: 0\">\n <checkbox\n [checked]=\"item.$selected ?? false\"\n [disabled]=\"item.count === 0\"\n [class]=\"'pointer-events-none'\" />\n </span>\n }\n @if (resolvedItemTemplate(); as tpl) {\n <ng-container\n [ngTemplateOutlet]=\"tpl\"\n [ngTemplateOutletContext]=\"{\n $implicit: item,\n field: field,\n selected: item.$selected ?? false,\n count: item.count,\n name: (listItemName(item) | syslang) ?? '',\n searchText: searchText()\n }\" />\n } @else {\n @let icon = $any(item).icon;\n @if (icon) {\n <FaIcon\n [faClass]=\"icon\"\n class=\"shrink-0 self-center justify-self-center\" />\n }\n @let appliesOnLabelClick = quickFilter() || applyOnSelect();\n <!-- Cut off, the label shows whole in a tooltip (`truncated` measures it); a label that fits has none. -->\n <span\n [class]=\"\n cn(\n 'line-clamp-1 cursor-pointer break-all text-ellipsis select-none',\n appliesOnLabelClick && 'hover:underline'\n )\n \"\n truncated\n #cut=\"truncated\"\n [tooltip]=\"listItemName(item) | syslang\"\n [tooltipDisabled]=\"!cut.isTruncated()\"\n (click)=\"onListItemTextClick(item, $event)\">\n @for (\n chunk of (listItemName(item) | syslang) ?? \"\"\n | highlightWord: searchText() : 10;\n track $index\n ) {\n <span [class]=\"{ 'font-bold': chunk.match }\" aria-hidden=\"true\">{{\n chunk.text\n }}</span>\n }\n </span>\n @if (showCount() && item.count > 0) {\n <span class=\"ml-auto px-1 text-xs empty:hidden\" aria-hidden=\"true\">{{\n item.count\n }}</span>\n }\n }\n </a>\n</ng-template>\n",
26927
27058
  dependencies: [
26928
27059
  {
26929
27060
  kind: "directive",
@@ -26974,12 +27105,45 @@ var AggregationListComponent = class AggregationListComponent {
26974
27105
  selector: "fa-icon, FaIcon",
26975
27106
  inputs: ["faClass", "class"]
26976
27107
  },
27108
+ {
27109
+ kind: "directive",
27110
+ type: TooltipDirective,
27111
+ selector: "[tooltip]",
27112
+ inputs: [
27113
+ "tooltip",
27114
+ "tooltipPosition",
27115
+ "tooltipEvent",
27116
+ "showDelay",
27117
+ "hideDelay",
27118
+ "life",
27119
+ "tooltipOffset",
27120
+ "autoHide",
27121
+ "hideOnEscape",
27122
+ "escape",
27123
+ "tooltipDisabled",
27124
+ "tooltipArrow",
27125
+ "tooltipStyleClass",
27126
+ "tooltipOptions",
27127
+ "tooltip-position",
27128
+ "tooltip-delay",
27129
+ "tooltip-duration",
27130
+ "tooltip-offset"
27131
+ ],
27132
+ exportAs: ["tooltip"]
27133
+ },
26977
27134
  {
26978
27135
  kind: "component",
26979
27136
  type: TriangleAlertIcon,
26980
27137
  selector: "triangle-alert, triangle-alert-icon, TriangleAlert, TriangleAlertIcon",
26981
27138
  inputs: ["class"]
26982
27139
  },
27140
+ {
27141
+ kind: "directive",
27142
+ type: TruncatedDirective,
27143
+ selector: "[truncated]",
27144
+ inputs: ["truncated"],
27145
+ exportAs: ["truncated"]
27146
+ },
26983
27147
  {
26984
27148
  kind: "component",
26985
27149
  type: AggregationPanelComponent,
@@ -27060,7 +27224,9 @@ i0.ɵɵngDeclareClassMetadata({
27060
27224
  SyslangPipe,
27061
27225
  TranslocoPipe,
27062
27226
  FaIconComponent,
27227
+ TooltipDirective,
27063
27228
  TriangleAlertIcon,
27229
+ TruncatedDirective,
27064
27230
  HighlightWordPipe,
27065
27231
  AggregationPanelComponent,
27066
27232
  CheckboxComponent
@@ -27069,7 +27235,7 @@ i0.ɵɵngDeclareClassMetadata({
27069
27235
  "[class]": "cn(\"flex h-[inherit] max-h-[inherit] flex-col\", class(), (isMissing() || (hideWhenEmpty() && isEmpty())) && \"hidden\")",
27070
27236
  "(keydown.enter)": "onHostEnterKey($event)"
27071
27237
  },
27072
- template: "@if (aggregation()?.isTree) {\n <div class=\"p-2 text-sm text-red-500\">\n <triangle-alert-icon class=\"mr-1 shrink-0\" />\n The aggregation component no longer supports tree aggregations. Please use\n the &lt;AggregationTree /&gt; component instead.\n </div>\n}\n<aggregation-panel\n [id]=\"id()\"\n [collapsible]=\"collapsible()\"\n [collapsed]=\"collapsed()\"\n [isEmpty]=\"isEmpty()\"\n [isDate]=\"isDate()\"\n [aggregation]=\"aggregation()\"\n [showFiltersCount]=\"showFiltersCount()\"\n [filtersCount]=\"filtersCount()\"\n [hasFilters]=\"hasFilters()\"\n [selection]=\"selection()\"\n [isAllSelected]=\"isAllSelected()\"\n [headless]=\"headless()\"\n [selectedCount]=\"selectedValues().length\"\n [(searchText)]=\"searchText\"\n [itemsLength]=\"items().length\"\n [hasMore]=\"aggregation()?.$hasMore ?? false\"\n [searchedItemsLength]=\"searchedItems().length\"\n (cleared)=\"clear()\"\n (applied)=\"apply()\"\n (allSelected)=\"selectAll()\"\n (allUnselected)=\"unselectAll()\"\n (loadedMore)=\"loadMore()\">\n <!-- Relay the host's `label` projection into the panel's `label` slot. When the\n host projects nothing, the local <ng-content> falls back to the icon + display\n (mirrors AggregationPanel's own fallback), so the header is never empty. -->\n <ng-container ngProjectAs=\"label\">\n <ng-content select=\"label\">\n @let labelIcon = aggregation()?.icon;\n @if (labelIcon) {\n <FaIcon [faClass]=\"labelIcon\" class=\"mr-1 shrink-0\" />\n }\n <span class=\"grow truncate\">{{\n aggregation()?.display | syslang | transloco\n }}</span>\n </ng-content>\n </ng-container>\n <div\n #scrollElement\n class=\"min-h-0 w-full flex-1 scrollbar-thin overflow-auto px-1 pt-1 pb-3\">\n <div\n ngListbox\n multi\n selectionMode=\"explicit\"\n [value]=\"selectedValues()\"\n (valueChange)=\"syncListboxValue($event)\"\n [wrap]=\"false\"\n [softDisabled]=\"false\"\n class=\"relative w-full\"\n [style.height]=\"virtualizer.getTotalSize() + 'px'\"\n role=\"listbox\"\n aria-multiselectable=\"true\"\n [attr.aria-label]=\"aggregation()?.display | syslang | transloco\">\n @for (vItem of virtualizer.getVirtualItems(); track vItem.index) {\n @let item = items()[vItem.index];\n @let description = rowDescription(item);\n <div\n #row\n ngOption\n [attr.aria-describedby]=\"\n description ? descriptionId(vItem.index) : null\n \"\n [attr.data-index]=\"vItem.index\"\n [value]=\"item.value\"\n [disabled]=\"item.count === 0\"\n [class]=\"cn('absolute w-full', menuItemBaseClass)\"\n (keydown.enter)=\"onOptionEnterKey(item, $event)\"\n (keydown.space)=\"\n onListItemRowClick(item, $event); $event.preventDefault()\n \"\n [style.transform]=\"'translateY(' + vItem.start + 'px)'\"\n role=\"option\">\n <ng-container\n [ngTemplateOutlet]=\"listItemTpl\"\n [ngTemplateOutletContext]=\"{\n $implicit: item,\n field: aggregation()?.column\n }\">\n </ng-container>\n @if (description) {\n <!-- `hidden`, not `sr-only`: read through aria-describedby only. A visually-hidden\n span would also be counted in the option's name (computed from its content). -->\n <span hidden [id]=\"descriptionId(vItem.index)\">{{\n description\n }}</span>\n }\n </div>\n }\n </div>\n </div>\n</aggregation-panel>\n\n<ng-template #listItemTpl let-item let-field=\"field\">\n <a\n [attr.aria-label]=\"listItemName(item) | syslang\"\n [class]=\"\n cn(\n '-m-1.5 flex min-w-0 grow items-center gap-2 leading-6',\n item.count === 0 && 'pointer-events-none text-(--font-neutral-muted)'\n )\n \"\n (click)=\"onListItemRowClick(item, $event)\">\n @if (showCheckbox()) {\n <!-- `inert` keeps the native input out of the tab order and out of hit-testing:\n keyboard navigation belongs to ngListbox/ngOption (roving tabindex), the\n selection is already exposed by role=\"option\" + aria-selected, and the click\n must reach the <a> above. Same treatment as the tree.\n --spacing-xs: 0 kills the gap the checkbox keeps between its box and its\n (here empty) text block. -->\n <span\n inert\n aria-hidden=\"true\"\n class=\"inline-flex shrink-0\"\n style=\"--spacing-xs: 0\">\n <checkbox\n [checked]=\"item.$selected ?? false\"\n [disabled]=\"item.count === 0\"\n [class]=\"'pointer-events-none'\" />\n </span>\n }\n @if (resolvedItemTemplate(); as tpl) {\n <ng-container\n [ngTemplateOutlet]=\"tpl\"\n [ngTemplateOutletContext]=\"{\n $implicit: item,\n field: field,\n selected: item.$selected ?? false,\n count: item.count,\n name: (listItemName(item) | syslang) ?? '',\n searchText: searchText()\n }\" />\n } @else {\n @let icon = $any(item).icon;\n @if (icon) {\n <FaIcon\n [faClass]=\"icon\"\n class=\"shrink-0 self-center justify-self-center\" />\n }\n @let appliesOnLabelClick = quickFilter() || applyOnSelect();\n <span\n [class]=\"\n cn(\n 'line-clamp-1 cursor-pointer break-all text-ellipsis select-none',\n appliesOnLabelClick && 'hover:underline'\n )\n \"\n [title]=\"\n appliesOnLabelClick\n ? ((isListItemFiltered(item, field)\n ? 'filters.removeFilter'\n : 'filters.addFilter'\n ) | transloco) +\n ': ' +\n (listItemName(item) | syslang)\n : (listItemName(item) | syslang)\n \"\n (click)=\"onListItemTextClick(item, $event)\">\n @for (\n chunk of (listItemName(item) | syslang) ?? \"\"\n | highlightWord: searchText() : 10;\n track $index\n ) {\n <span [class]=\"{ 'font-bold': chunk.match }\" aria-hidden=\"true\">{{\n chunk.text\n }}</span>\n }\n </span>\n @if (showCount() && item.count > 0) {\n <span class=\"ml-auto px-1 text-xs empty:hidden\" aria-hidden=\"true\">{{\n item.count\n }}</span>\n }\n }\n </a>\n</ng-template>\n"
27238
+ template: "@if (aggregation()?.isTree) {\n <div class=\"p-2 text-sm text-red-500\">\n <triangle-alert-icon class=\"mr-1 shrink-0\" />\n The aggregation component no longer supports tree aggregations. Please use\n the &lt;AggregationTree /&gt; component instead.\n </div>\n}\n<aggregation-panel\n [id]=\"id()\"\n [collapsible]=\"collapsible()\"\n [collapsed]=\"collapsed()\"\n [isEmpty]=\"isEmpty()\"\n [isDate]=\"isDate()\"\n [aggregation]=\"aggregation()\"\n [showFiltersCount]=\"showFiltersCount()\"\n [filtersCount]=\"filtersCount()\"\n [hasFilters]=\"hasFilters()\"\n [selection]=\"selection()\"\n [isAllSelected]=\"isAllSelected()\"\n [headless]=\"headless()\"\n [selectedCount]=\"selectedValues().length\"\n [(searchText)]=\"searchText\"\n [itemsLength]=\"items().length\"\n [hasMore]=\"aggregation()?.$hasMore ?? false\"\n [searchedItemsLength]=\"searchedItems().length\"\n (cleared)=\"clear()\"\n (applied)=\"apply()\"\n (allSelected)=\"selectAll()\"\n (allUnselected)=\"unselectAll()\"\n (loadedMore)=\"loadMore()\">\n <!-- Relay the host's `label` projection into the panel's `label` slot. When the\n host projects nothing, the local <ng-content> falls back to the icon + display\n (mirrors AggregationPanel's own fallback), so the header is never empty. -->\n <ng-container ngProjectAs=\"label\">\n <ng-content select=\"label\">\n @let labelIcon = aggregation()?.icon;\n @if (labelIcon) {\n <FaIcon [faClass]=\"labelIcon\" class=\"mr-1 shrink-0\" />\n }\n <span class=\"grow truncate\">{{\n aggregation()?.display | syslang | transloco\n }}</span>\n </ng-content>\n </ng-container>\n <div\n #scrollElement\n class=\"min-h-0 w-full flex-1 scrollbar-thin overflow-auto px-1 pt-1 pb-3\">\n <div\n ngListbox\n multi\n selectionMode=\"explicit\"\n [value]=\"selectedValues()\"\n (valueChange)=\"syncListboxValue($event)\"\n [wrap]=\"false\"\n [softDisabled]=\"false\"\n class=\"relative w-full\"\n [style.height]=\"virtualizer.getTotalSize() + 'px'\"\n role=\"listbox\"\n aria-multiselectable=\"true\"\n [attr.aria-label]=\"aggregation()?.display | syslang | transloco\">\n @for (vItem of virtualizer.getVirtualItems(); track vItem.index) {\n @let item = items()[vItem.index];\n @let description = rowDescription(item);\n <div\n #row\n ngOption\n [attr.aria-describedby]=\"\n description ? descriptionId(vItem.index) : null\n \"\n [attr.data-index]=\"vItem.index\"\n [value]=\"item.value\"\n [disabled]=\"item.count === 0\"\n [class]=\"cn('absolute w-full', menuItemBaseClass)\"\n (keydown.enter)=\"onOptionEnterKey(item, $event)\"\n (keydown.space)=\"\n onListItemRowClick(item, $event); $event.preventDefault()\n \"\n [style.transform]=\"'translateY(' + vItem.start + 'px)'\"\n role=\"option\">\n <ng-container\n [ngTemplateOutlet]=\"listItemTpl\"\n [ngTemplateOutletContext]=\"{\n $implicit: item,\n field: aggregation()?.column\n }\">\n </ng-container>\n @if (description) {\n <!-- `hidden`, not `sr-only`: read through aria-describedby only. A visually-hidden\n span would also be counted in the option's name (computed from its content). -->\n <span hidden [id]=\"descriptionId(vItem.index)\">{{\n description\n }}</span>\n }\n </div>\n }\n </div>\n </div>\n</aggregation-panel>\n\n<ng-template #listItemTpl let-item let-field=\"field\">\n <a\n [attr.aria-label]=\"listItemName(item) | syslang\"\n [class]=\"\n cn(\n '-m-1.5 flex min-w-0 grow items-center gap-2 leading-6',\n item.count === 0 && 'pointer-events-none text-(--font-neutral-muted)'\n )\n \"\n (click)=\"onListItemRowClick(item, $event)\">\n @if (showCheckbox()) {\n <!-- `inert` keeps the native input out of the tab order and out of hit-testing:\n keyboard navigation belongs to ngListbox/ngOption (roving tabindex), the\n selection is already exposed by role=\"option\" + aria-selected, and the click\n must reach the <a> above. Same treatment as the tree.\n --spacing-xs: 0 kills the gap the checkbox keeps between its box and its\n (here empty) text block. -->\n <span\n inert\n aria-hidden=\"true\"\n class=\"inline-flex shrink-0\"\n style=\"--spacing-xs: 0\">\n <checkbox\n [checked]=\"item.$selected ?? false\"\n [disabled]=\"item.count === 0\"\n [class]=\"'pointer-events-none'\" />\n </span>\n }\n @if (resolvedItemTemplate(); as tpl) {\n <ng-container\n [ngTemplateOutlet]=\"tpl\"\n [ngTemplateOutletContext]=\"{\n $implicit: item,\n field: field,\n selected: item.$selected ?? false,\n count: item.count,\n name: (listItemName(item) | syslang) ?? '',\n searchText: searchText()\n }\" />\n } @else {\n @let icon = $any(item).icon;\n @if (icon) {\n <FaIcon\n [faClass]=\"icon\"\n class=\"shrink-0 self-center justify-self-center\" />\n }\n @let appliesOnLabelClick = quickFilter() || applyOnSelect();\n <!-- Cut off, the label shows whole in a tooltip (`truncated` measures it); a label that fits has none. -->\n <span\n [class]=\"\n cn(\n 'line-clamp-1 cursor-pointer break-all text-ellipsis select-none',\n appliesOnLabelClick && 'hover:underline'\n )\n \"\n truncated\n #cut=\"truncated\"\n [tooltip]=\"listItemName(item) | syslang\"\n [tooltipDisabled]=\"!cut.isTruncated()\"\n (click)=\"onListItemTextClick(item, $event)\">\n @for (\n chunk of (listItemName(item) | syslang) ?? \"\"\n | highlightWord: searchText() : 10;\n track $index\n ) {\n <span [class]=\"{ 'font-bold': chunk.match }\" aria-hidden=\"true\">{{\n chunk.text\n }}</span>\n }\n </span>\n @if (showCount() && item.count > 0) {\n <span class=\"ml-auto px-1 text-xs empty:hidden\" aria-hidden=\"true\">{{\n item.count\n }}</span>\n }\n }\n </a>\n</ng-template>\n"
27073
27239
  }]
27074
27240
  }],
27075
27241
  ctorParameters: () => [],
@@ -28095,6 +28261,17 @@ function chipsForLeaf(leaf, source) {
28095
28261
  }
28096
28262
  var AppliedFiltersComponent = class AppliedFiltersComponent {
28097
28263
  queryParamsStore = inject(QueryParamsStore);
28264
+ static nextId = 0;
28265
+ listId = `applied-filters-${AppliedFiltersComponent.nextId++}`;
28266
+ list = viewChild("list", ...ngDevMode ? [{ debugName: "list" }] : /* istanbul ignore next */ []);
28267
+ foldable = signal(false, ...ngDevMode ? [{ debugName: "foldable" }] : /* istanbul ignore next */ []);
28268
+ expanded = signal(false, ...ngDevMode ? [{ debugName: "expanded" }] : /* istanbul ignore next */ []);
28269
+ foldedHeight = signal(0, ...ngDevMode ? [{ debugName: "foldedHeight" }] : /* istanbul ignore next */ []);
28270
+ fullHeight = signal(0, ...ngDevMode ? [{ debugName: "fullHeight" }] : /* istanbul ignore next */ []);
28271
+ listHeight = computed(() => this.foldable() ? this.expanded() ? this.fullHeight() : this.foldedHeight() : null, ...ngDevMode ? [{ debugName: "listHeight" }] : /* istanbul ignore next */ []);
28272
+ toggle() {
28273
+ this.expanded.update((open) => !open);
28274
+ }
28098
28275
  filters = input(void 0, ...ngDevMode ? [{ debugName: "filters" }] : /* istanbul ignore next */ []);
28099
28276
  syncUrl = input(true, {
28100
28277
  ...ngDevMode ? { debugName: "syncUrl" } : /* istanbul ignore next */ {},
@@ -28108,10 +28285,62 @@ var AppliedFiltersComponent = class AppliedFiltersComponent {
28108
28285
  scheme = input("grey", ...ngDevMode ? [{ debugName: "scheme" }] : /* istanbul ignore next */ []);
28109
28286
  size = input("sm", ...ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []);
28110
28287
  chipClass = input("", ...ngDevMode ? [{ debugName: "chipClass" }] : /* istanbul ignore next */ []);
28288
+ maxLines = input(2, {
28289
+ ...ngDevMode ? { debugName: "maxLines" } : /* istanbul ignore next */ {},
28290
+ transform: numberAttribute
28291
+ });
28292
+ showClearAll = input(true, {
28293
+ ...ngDevMode ? { debugName: "showClearAll" } : /* istanbul ignore next */ {},
28294
+ transform: booleanAttribute
28295
+ });
28111
28296
  removed = output();
28112
28297
  cleared = output();
28113
28298
  filtersChange = output();
28114
28299
  changed = output();
28300
+ constructor() {
28301
+ const destroyRef = inject(DestroyRef);
28302
+ afterRenderEffect(() => {
28303
+ this.chips();
28304
+ this.maxLines();
28305
+ this.expanded();
28306
+ this.measure();
28307
+ });
28308
+ afterRenderEffect((onCleanup) => {
28309
+ const element = this.list()?.nativeElement;
28310
+ if (!element || typeof ResizeObserver === "undefined") return;
28311
+ const observer = new ResizeObserver(() => this.measure());
28312
+ observer.observe(element);
28313
+ onCleanup(() => observer.disconnect());
28314
+ });
28315
+ destroyRef.onDestroy(() => this.foldable.set(false));
28316
+ }
28317
+ measure() {
28318
+ const element = this.list()?.nativeElement;
28319
+ const max = this.maxLines();
28320
+ if (!element || max <= 0) {
28321
+ this.foldable.set(false);
28322
+ return;
28323
+ }
28324
+ const items = Array.from(element.children);
28325
+ const first = items[0];
28326
+ if (!first) {
28327
+ this.foldable.set(false);
28328
+ return;
28329
+ }
28330
+ const origin = first.offsetTop;
28331
+ const tops = Array.from(new Set(items.map((item) => item.offsetTop - origin))).sort((a, b) => a - b);
28332
+ const gap = Number.parseFloat(getComputedStyle(element).rowGap) || 0;
28333
+ if (tops.length <= max) {
28334
+ this.foldable.set(false);
28335
+ items.forEach((item) => item.inert = false);
28336
+ return;
28337
+ }
28338
+ this.foldable.set(true);
28339
+ this.foldedHeight.set(Math.max(0, tops[max] - gap));
28340
+ this.fullHeight.set(element.scrollHeight);
28341
+ const folded = this.foldedHeight();
28342
+ items.forEach((item) => item.inert = !this.expanded() && item.offsetTop - origin >= folded);
28343
+ }
28115
28344
  chips = computed(() => {
28116
28345
  return (this.filters() ?? this.queryParamsStore.filters()).flatMap((source) => filterLeaves(source).flatMap((leaf) => chipsForLeaf(leaf, source)));
28117
28346
  }, ...ngDevMode ? [{ debugName: "chips" }] : /* istanbul ignore next */ []);
@@ -28207,6 +28436,20 @@ var AppliedFiltersComponent = class AppliedFiltersComponent {
28207
28436
  isSignal: true,
28208
28437
  isRequired: false,
28209
28438
  transformFunction: null
28439
+ },
28440
+ maxLines: {
28441
+ classPropertyName: "maxLines",
28442
+ publicName: "maxLines",
28443
+ isSignal: true,
28444
+ isRequired: false,
28445
+ transformFunction: null
28446
+ },
28447
+ showClearAll: {
28448
+ classPropertyName: "showClearAll",
28449
+ publicName: "showClearAll",
28450
+ isSignal: true,
28451
+ isRequired: false,
28452
+ transformFunction: null
28210
28453
  }
28211
28454
  },
28212
28455
  outputs: {
@@ -28217,39 +28460,75 @@ var AppliedFiltersComponent = class AppliedFiltersComponent {
28217
28460
  },
28218
28461
  host: { properties: { "class.hidden": "chips().length === 0" } },
28219
28462
  providers: [provideTranslocoScope("filters")],
28463
+ viewQueries: [{
28464
+ propertyName: "list",
28465
+ first: true,
28466
+ predicate: ["list"],
28467
+ descendants: true,
28468
+ isSignal: true
28469
+ }],
28220
28470
  ngImport: i0,
28221
28471
  template: `
28222
28472
  @if (chips().length > 0) {
28223
- <div class="flex flex-wrap items-center gap-1.5" role="list">
28224
- @for (chip of chips(); track chip.id) {
28225
- <tag
28226
- role="listitem"
28227
- [variant]="variant()"
28228
- [scheme]="scheme()"
28229
- [size]="size()"
28230
- [class]="'max-w-full items-center gap-1 ' + chipClass()"
28231
- >
28232
- @if (showField()) {
28233
- <span>{{ (chip.name ?? chip.field) | syslang }}:</span>
28234
- }
28235
- <span class="truncate">{{ chip.display | syslang }}</span>
28236
- <button
28237
- variant="none"
28238
- size="xs"
28239
- [iconOnly]="true"
28240
- class="shrink-0"
28241
- [attr.aria-label]="('filters.removeFilter' | transloco) + ': ' + chip.display"
28242
- [title]="('filters.removeFilter' | transloco) + ': ' + chip.display"
28243
- (click)="remove(chip)"
28473
+ <!-- The zone the "show more" button belongs to: it appears when the pointer is on it or the focus is in it. -->
28474
+ <div class="group/applied relative" [class.pb-7]="foldable() && expanded()">
28475
+ <!-- Past maxLines lines the list is folded to that height — the rest is behind a fade, and a height that grows and
28476
+ shrinks shows it. The chips that fall in the folded part are inert (see measure): a remove button nobody can see
28477
+ must not stay in the tab order. The clip keeps a margin of a few pixels, so the focus ring of the visible chips is whole. -->
28478
+ <div
28479
+ #list
28480
+ [id]="listId"
28481
+ class="flex flex-wrap content-start items-center gap-1.5 [overflow-clip-margin:0.25rem] overflow-clip transition-[height] duration-200 ease-[cubic-bezier(0.32,0.72,0,1)] motion-reduce:transition-none"
28482
+ [style.height.px]="listHeight()"
28483
+ [style.mask-image]="foldable() && !expanded() ? 'linear-gradient(to bottom, black calc(100% - 1.5rem), transparent)' : null"
28484
+ role="list"
28485
+ >
28486
+ @for (chip of chips(); track chip.id) {
28487
+ <tag
28488
+ role="listitem"
28489
+ [variant]="variant()"
28490
+ [scheme]="scheme()"
28491
+ [size]="size()"
28492
+ [class]="'max-w-full items-center gap-1 ' + chipClass()"
28244
28493
  >
28245
- <x-mark-icon class="size-3" />
28494
+ @if (showField()) {
28495
+ <span>{{ (chip.name ?? chip.field) | syslang }}:</span>
28496
+ }
28497
+ <span class="truncate">{{ chip.display | syslang }}</span>
28498
+ <button
28499
+ variant="none"
28500
+ size="xs"
28501
+ [iconOnly]="true"
28502
+ class="shrink-0"
28503
+ [attr.aria-label]="('filters.removeFilter' | transloco) + ': ' + chip.display"
28504
+ [title]="('filters.removeFilter' | transloco) + ': ' + chip.display"
28505
+ (click)="remove(chip)"
28506
+ >
28507
+ <x-mark-icon class="size-3" />
28508
+ </button>
28509
+ </tag>
28510
+ }
28511
+
28512
+ @if (showClearAll() && chips().length > 1) {
28513
+ <button variant="tertiary" size="xs" (click)="clear()">
28514
+ {{ "filters.clearAllFilters" | transloco }}
28246
28515
  </button>
28247
- </tag>
28248
- }
28516
+ }
28517
+ </div>
28249
28518
 
28250
- @if (chips().length > 1) {
28251
- <button variant="tertiary" size="xs" (click)="clear()">
28252
- {{ "filters.clearAllFilters" | transloco }}
28519
+ @if (foldable()) {
28520
+ <!-- Out of sight until the zone is hovered or holds the focus — and always there on a screen with no hover. Still in the
28521
+ tab order, and revealed by the focus. Its own surface, since it lies over the chips. -->
28522
+ <button
28523
+ type="button"
28524
+ variant="secondary"
28525
+ size="xs"
28526
+ class="absolute right-0 bottom-0 bg-(--bg-neutral-white) opacity-0 shadow-(--shadow-1) transition-opacity duration-150 group-hover/applied:opacity-100 group-focus-within/applied:opacity-100 focus-visible:opacity-100 motion-reduce:transition-none [@media(hover:none)]:opacity-100"
28527
+ [attr.aria-expanded]="expanded()"
28528
+ [attr.aria-controls]="listId"
28529
+ (click)="toggle()"
28530
+ >
28531
+ {{ (expanded() ? "filters.showLess" : "filters.showMore") | transloco }}
28253
28532
  </button>
28254
28533
  }
28255
28534
  </div>
@@ -28320,36 +28599,65 @@ i0.ɵɵngDeclareClassMetadata({
28320
28599
  providers: [provideTranslocoScope("filters")],
28321
28600
  template: `
28322
28601
  @if (chips().length > 0) {
28323
- <div class="flex flex-wrap items-center gap-1.5" role="list">
28324
- @for (chip of chips(); track chip.id) {
28325
- <tag
28326
- role="listitem"
28327
- [variant]="variant()"
28328
- [scheme]="scheme()"
28329
- [size]="size()"
28330
- [class]="'max-w-full items-center gap-1 ' + chipClass()"
28331
- >
28332
- @if (showField()) {
28333
- <span>{{ (chip.name ?? chip.field) | syslang }}:</span>
28334
- }
28335
- <span class="truncate">{{ chip.display | syslang }}</span>
28336
- <button
28337
- variant="none"
28338
- size="xs"
28339
- [iconOnly]="true"
28340
- class="shrink-0"
28341
- [attr.aria-label]="('filters.removeFilter' | transloco) + ': ' + chip.display"
28342
- [title]="('filters.removeFilter' | transloco) + ': ' + chip.display"
28343
- (click)="remove(chip)"
28602
+ <!-- The zone the "show more" button belongs to: it appears when the pointer is on it or the focus is in it. -->
28603
+ <div class="group/applied relative" [class.pb-7]="foldable() && expanded()">
28604
+ <!-- Past maxLines lines the list is folded to that height — the rest is behind a fade, and a height that grows and
28605
+ shrinks shows it. The chips that fall in the folded part are inert (see measure): a remove button nobody can see
28606
+ must not stay in the tab order. The clip keeps a margin of a few pixels, so the focus ring of the visible chips is whole. -->
28607
+ <div
28608
+ #list
28609
+ [id]="listId"
28610
+ class="flex flex-wrap content-start items-center gap-1.5 [overflow-clip-margin:0.25rem] overflow-clip transition-[height] duration-200 ease-[cubic-bezier(0.32,0.72,0,1)] motion-reduce:transition-none"
28611
+ [style.height.px]="listHeight()"
28612
+ [style.mask-image]="foldable() && !expanded() ? 'linear-gradient(to bottom, black calc(100% - 1.5rem), transparent)' : null"
28613
+ role="list"
28614
+ >
28615
+ @for (chip of chips(); track chip.id) {
28616
+ <tag
28617
+ role="listitem"
28618
+ [variant]="variant()"
28619
+ [scheme]="scheme()"
28620
+ [size]="size()"
28621
+ [class]="'max-w-full items-center gap-1 ' + chipClass()"
28344
28622
  >
28345
- <x-mark-icon class="size-3" />
28623
+ @if (showField()) {
28624
+ <span>{{ (chip.name ?? chip.field) | syslang }}:</span>
28625
+ }
28626
+ <span class="truncate">{{ chip.display | syslang }}</span>
28627
+ <button
28628
+ variant="none"
28629
+ size="xs"
28630
+ [iconOnly]="true"
28631
+ class="shrink-0"
28632
+ [attr.aria-label]="('filters.removeFilter' | transloco) + ': ' + chip.display"
28633
+ [title]="('filters.removeFilter' | transloco) + ': ' + chip.display"
28634
+ (click)="remove(chip)"
28635
+ >
28636
+ <x-mark-icon class="size-3" />
28637
+ </button>
28638
+ </tag>
28639
+ }
28640
+
28641
+ @if (showClearAll() && chips().length > 1) {
28642
+ <button variant="tertiary" size="xs" (click)="clear()">
28643
+ {{ "filters.clearAllFilters" | transloco }}
28346
28644
  </button>
28347
- </tag>
28348
- }
28645
+ }
28646
+ </div>
28349
28647
 
28350
- @if (chips().length > 1) {
28351
- <button variant="tertiary" size="xs" (click)="clear()">
28352
- {{ "filters.clearAllFilters" | transloco }}
28648
+ @if (foldable()) {
28649
+ <!-- Out of sight until the zone is hovered or holds the focus — and always there on a screen with no hover. Still in the
28650
+ tab order, and revealed by the focus. Its own surface, since it lies over the chips. -->
28651
+ <button
28652
+ type="button"
28653
+ variant="secondary"
28654
+ size="xs"
28655
+ class="absolute right-0 bottom-0 bg-(--bg-neutral-white) opacity-0 shadow-(--shadow-1) transition-opacity duration-150 group-hover/applied:opacity-100 group-focus-within/applied:opacity-100 focus-visible:opacity-100 motion-reduce:transition-none [@media(hover:none)]:opacity-100"
28656
+ [attr.aria-expanded]="expanded()"
28657
+ [attr.aria-controls]="listId"
28658
+ (click)="toggle()"
28659
+ >
28660
+ {{ (expanded() ? "filters.showLess" : "filters.showMore") | transloco }}
28353
28661
  </button>
28354
28662
  }
28355
28663
  </div>
@@ -28358,7 +28666,12 @@ i0.ɵɵngDeclareClassMetadata({
28358
28666
  host: { "[class.hidden]": "chips().length === 0" }
28359
28667
  }]
28360
28668
  }],
28669
+ ctorParameters: () => [],
28361
28670
  propDecorators: {
28671
+ list: [{
28672
+ type: i0.ViewChild,
28673
+ args: ["list", { isSignal: true }]
28674
+ }],
28362
28675
  filters: [{
28363
28676
  type: i0.Input,
28364
28677
  args: [{
@@ -28415,6 +28728,22 @@ i0.ɵɵngDeclareClassMetadata({
28415
28728
  required: false
28416
28729
  }]
28417
28730
  }],
28731
+ maxLines: [{
28732
+ type: i0.Input,
28733
+ args: [{
28734
+ isSignal: true,
28735
+ alias: "maxLines",
28736
+ required: false
28737
+ }]
28738
+ }],
28739
+ showClearAll: [{
28740
+ type: i0.Input,
28741
+ args: [{
28742
+ isSignal: true,
28743
+ alias: "showClearAll",
28744
+ required: false
28745
+ }]
28746
+ }],
28418
28747
  removed: [{
28419
28748
  type: i0.Output,
28420
28749
  args: ["removed"]
@@ -31619,6 +31948,1049 @@ i0.ɵɵngDeclareClassMetadata({
31619
31948
  }]
31620
31949
  }
31621
31950
  });
31951
+ function filterLabel(filter) {
31952
+ return filter.name || filter.field || "";
31953
+ }
31954
+ function filterValue(filter) {
31955
+ if (filter.filters?.length) return filter.filters.map((f) => filterLabel(f) ? `${filterLabel(f)}: ${filterValue(f)}` : filterValue(f)).join(", ");
31956
+ if (filter.values?.length) return filter.values.join(", ");
31957
+ return filter.display ?? filter.value ?? [filter.start, filter.end].filter((v) => v !== void 0 && v !== "").join(" – ");
31958
+ }
31959
+ var QuickFiltersListComponent = class QuickFiltersListComponent {
31960
+ searchable = input(false, ...ngDevMode ? [{ debugName: "searchable" }] : /* istanbul ignore next */ []);
31961
+ deletable = input(true, ...ngDevMode ? [{ debugName: "deletable" }] : /* istanbul ignore next */ []);
31962
+ emptyHint = input(...ngDevMode ? [void 0, { debugName: "emptyHint" }] : /* istanbul ignore next */ []);
31963
+ applied = output();
31964
+ deleted = output();
31965
+ userSettingsStore = inject(UserSettingsStore);
31966
+ queryParamsStore = inject(QueryParamsStore);
31967
+ appStore = inject(AppStore);
31968
+ transloco = inject(TranslocoService);
31969
+ host = inject(ElementRef);
31970
+ injector = inject(Injector);
31971
+ deleteKeys = isApplePlatform() ? ["Delete", "Backspace"] : ["Delete"];
31972
+ quickFilters = computed(() => this.userSettingsStore.quickFilters() ?? [], ...ngDevMode ? [{ debugName: "quickFilters" }] : /* istanbul ignore next */ []);
31973
+ query = signal("", ...ngDevMode ? [{ debugName: "query" }] : /* istanbul ignore next */ []);
31974
+ filtered = computed(() => {
31975
+ const query = this.query().trim().toLowerCase();
31976
+ return query ? this.quickFilters().filter((quickFilter) => quickFilter.name.toLowerCase().includes(query)) : this.quickFilters();
31977
+ }, ...ngDevMode ? [{ debugName: "filtered" }] : /* istanbul ignore next */ []);
31978
+ announcement = signal("", ...ngDevMode ? [{ debugName: "announcement" }] : /* istanbul ignore next */ []);
31979
+ details = signal(null, ...ngDevMode ? [{ debugName: "details" }] : /* istanbul ignore next */ []);
31980
+ detailsCard = viewChild("detailsCard", ...ngDevMode ? [{ debugName: "detailsCard" }] : /* istanbul ignore next */ []);
31981
+ hideTimer;
31982
+ deleting = signal(false, ...ngDevMode ? [{ debugName: "deleting" }] : /* istanbul ignore next */ []);
31983
+ constructor() {
31984
+ injectFloatingPanel({
31985
+ panel: () => this.detailsCard()?.nativeElement,
31986
+ anchor: () => this.details()?.anchor,
31987
+ placement: "bottom-start",
31988
+ offset: 4,
31989
+ topLayer: true
31990
+ });
31991
+ inject(DestroyRef).onDestroy(() => this.cancelHideDetails());
31992
+ }
31993
+ chips(quickFilter) {
31994
+ return quickFilter.filters.map((filter) => {
31995
+ const icon = (filter.field ? this.appStore.getAggregationCustomization(filter.field, filter.name) : void 0)?.icon;
31996
+ const value = filterValue(filter);
31997
+ const label = filterLabel(filter);
31998
+ return {
31999
+ text: icon || !label || !value ? value || label : `${label}: ${value}`,
32000
+ icon
32001
+ };
32002
+ }).filter((chip) => chip.text);
32003
+ }
32004
+ showDetails(quickFilter, anchor) {
32005
+ this.cancelHideDetails();
32006
+ this.details.set({
32007
+ quickFilter,
32008
+ anchor
32009
+ });
32010
+ }
32011
+ scheduleHideDetails() {
32012
+ this.cancelHideDetails();
32013
+ this.hideTimer = setTimeout(() => this.details.set(null), 100);
32014
+ }
32015
+ cancelHideDetails() {
32016
+ clearTimeout(this.hideTimer);
32017
+ }
32018
+ onEscape(event) {
32019
+ if (!this.details()) return;
32020
+ event.stopPropagation();
32021
+ this.cancelHideDetails();
32022
+ this.details.set(null);
32023
+ }
32024
+ picked = signal([], ...ngDevMode ? [{ debugName: "picked" }] : /* istanbul ignore next */ []);
32025
+ pick(values) {
32026
+ const quickFilter = values.at(-1);
32027
+ this.picked.set([]);
32028
+ if (quickFilter) this.apply(quickFilter);
32029
+ }
32030
+ apply(quickFilter) {
32031
+ this.cancelHideDetails();
32032
+ this.details.set(null);
32033
+ this.queryParamsStore.patch({ filters: structuredClone(quickFilter.filters) });
32034
+ this.applied.emit(quickFilter);
32035
+ }
32036
+ async remove(quickFilter, index, event) {
32037
+ event.stopPropagation();
32038
+ if (this.deleting()) return;
32039
+ this.deleting.set(true);
32040
+ this.cancelHideDetails();
32041
+ this.details.set(null);
32042
+ const hadFocus = this.host.nativeElement.contains(document.activeElement);
32043
+ try {
32044
+ await this.userSettingsStore.removeQuickFilter(quickFilter.id);
32045
+ const message = this.transloco.translate("quickFilters.deleted", { name: quickFilter.name });
32046
+ notify.success(message, { duration: 2e3 });
32047
+ this.announcement.set(message);
32048
+ this.deleted.emit(quickFilter);
32049
+ if (hadFocus) afterNextRender(() => {
32050
+ const rows = this.host.nativeElement.querySelectorAll("[role='option']");
32051
+ (rows.length ? rows[Math.min(index, rows.length - 1)] : this.host.nativeElement).focus();
32052
+ }, { injector: this.injector });
32053
+ } finally {
32054
+ this.deleting.set(false);
32055
+ }
32056
+ }
32057
+ static ɵfac = i0.ɵɵngDeclareFactory({
32058
+ minVersion: "12.0.0",
32059
+ version: "22.2.1",
32060
+ ngImport: i0,
32061
+ type: QuickFiltersListComponent,
32062
+ deps: [],
32063
+ target: i0.ɵɵFactoryTarget.Component
32064
+ });
32065
+ static ɵcmp = i0.ɵɵngDeclareComponent({
32066
+ minVersion: "17.0.0",
32067
+ version: "22.2.1",
32068
+ type: QuickFiltersListComponent,
32069
+ isStandalone: true,
32070
+ selector: "QuickFiltersList, quick-filters-list",
32071
+ inputs: {
32072
+ searchable: {
32073
+ classPropertyName: "searchable",
32074
+ publicName: "searchable",
32075
+ isSignal: true,
32076
+ isRequired: false,
32077
+ transformFunction: null
32078
+ },
32079
+ deletable: {
32080
+ classPropertyName: "deletable",
32081
+ publicName: "deletable",
32082
+ isSignal: true,
32083
+ isRequired: false,
32084
+ transformFunction: null
32085
+ },
32086
+ emptyHint: {
32087
+ classPropertyName: "emptyHint",
32088
+ publicName: "emptyHint",
32089
+ isSignal: true,
32090
+ isRequired: false,
32091
+ transformFunction: null
32092
+ }
32093
+ },
32094
+ outputs: {
32095
+ applied: "applied",
32096
+ deleted: "deleted"
32097
+ },
32098
+ host: {
32099
+ attributes: { "tabindex": "-1" },
32100
+ listeners: { "keydown.escape": "onEscape($event)" },
32101
+ classAttribute: "flex min-h-0 flex-col gap-(--spacing-xs) outline-none"
32102
+ },
32103
+ providers: [provideTranslocoScope("quick-filters")],
32104
+ viewQueries: [{
32105
+ propertyName: "detailsCard",
32106
+ first: true,
32107
+ predicate: ["detailsCard"],
32108
+ descendants: true,
32109
+ isSignal: true
32110
+ }],
32111
+ ngImport: i0,
32112
+ template: `
32113
+ @if (searchable() && quickFilters().length > 0) {
32114
+ <SearchInput
32115
+ size="md"
32116
+ class="border-(--stroke-primary-light)"
32117
+ [(value)]="query"
32118
+ [placeholder]="'quickFilters.search' | transloco"
32119
+ [aria-label]="'quickFilters.search' | transloco"
32120
+ [clearLabel]="'quickFilters.clearSearch' | transloco" />
32121
+ }
32122
+
32123
+ @if (filtered().length) {
32124
+ <!-- The galactik List, as the design's List Item: a listbox of menu rows with an icon, the name and the number of filters.
32125
+ Choosing a row (click, Enter) applies it; the selection is not kept. The cross at the end of the row deletes it. -->
32126
+ <List
32127
+ variant="list-menu"
32128
+ size="medium"
32129
+ selectionMode="explicit"
32130
+ class="-mx-1 min-h-0 flex-1 overflow-auto px-1 [scrollbar-color:color-mix(in_srgb,currentColor_40%,transparent)_transparent] [scrollbar-width:thin]"
32131
+ [attr.aria-label]="'quickFilters.list' | transloco"
32132
+ [value]="picked()"
32133
+ (valueChange)="pick($event)">
32134
+ @for (quickFilter of filtered(); track quickFilter.id; let index = $index) {
32135
+ <ListItem
32136
+ [value]="quickFilter"
32137
+ [label]="quickFilter.name"
32138
+ (focusin)="showDetails(quickFilter, count)"
32139
+ (focusout)="scheduleHideDetails()">
32140
+ <ListItemLeft>
32141
+ <ListItemIcon><quick-filter-icon /></ListItemIcon>
32142
+ <ListItemText>
32143
+ <!-- The tooltip shows the whole name, and only when it is cut off: a full name needs none. -->
32144
+ <span truncated #cut="truncated" [tooltip]="quickFilter.name" [tooltipDisabled]="!cut.isTruncated()" class="block truncate">{{ quickFilter.name }}</span>
32145
+ <!-- The line of the count of filters is the one that shows what the quick filter holds, on hover (the row itself only applies
32146
+ it); on the keyboard, the row having the focus shows it. It takes the look of the subtext of a row. -->
32147
+ <span
32148
+ #count
32149
+ data-count
32150
+ class="list-item-subtext block w-fit! decoration-dotted underline-offset-2 hover:underline"
32151
+ (mouseenter)="showDetails(quickFilter, count)"
32152
+ (mouseleave)="scheduleHideDetails()">
32153
+ {{ (quickFilter.filters.length === 1 ? "quickFilters.countOne" : "quickFilters.countOther") | transloco: { count: quickFilter.filters.length } }}
32154
+ </span>
32155
+ </ListItemText>
32156
+ </ListItemLeft>
32157
+ @if (deletable()) {
32158
+ <ListItemAction
32159
+ [ariaLabel]="'quickFilters.delete' | transloco: { name: quickFilter.name }"
32160
+ [keys]="deleteKeys"
32161
+ [shortcut]="'quickFilters.deleteShortcut' | transloco"
32162
+ [description]="'quickFilters.deleteDescription' | transloco: { key: 'quickFilters.deleteShortcut' | transloco }"
32163
+ (click)="remove(quickFilter, index, $event)">
32164
+ <x-mark-icon />
32165
+ </ListItemAction>
32166
+ }
32167
+ </ListItem>
32168
+ }
32169
+ </List>
32170
+
32171
+ <!-- The hover card of the design: the filters a quick filter holds, as chips, hanging under its row. Placed with Floating UI in
32172
+ the top layer, so it is above whatever clips the list. It stays while the pointer is on it, and while the focus is on the row. -->
32173
+ @if (details(); as shown) {
32174
+ <div
32175
+ #detailsCard
32176
+ role="tooltip"
32177
+ class="fixed m-0 flex max-w-72 flex-wrap gap-(--spacing-3xs) rounded-(--radius-lg) border border-solid border-(--stroke-neutral-white) bg-(--bg-neutral-white) p-(--spacing-md) shadow-(--shadow-2)"
32178
+ (mouseenter)="cancelHideDetails()"
32179
+ (mouseleave)="scheduleHideDetails()">
32180
+ @for (chip of chips(shown.quickFilter); track $index) {
32181
+ <span
32182
+ data-chip
32183
+ class="inline-flex max-w-full min-w-0 items-center gap-(--spacing-3xs) rounded-(--radius-rounded) bg-(--bg-neutral-base) py-(--spacing-4xs) pr-(--spacing-2xs) pl-(--spacing-2xs) text-(length:--size-sm) font-medium text-(--font-neutral-base)">
32184
+ <!-- the icon the application gives the aggregation in the filters custom JSON, as on the filter button -->
32185
+ @if (chip.icon) {
32186
+ <fa-icon [faClass]="chip.icon" class="shrink-0" />
32187
+ }
32188
+ <!-- A chip that is cut off shows its whole text in a tooltip; one that fits has none. -->
32189
+ <span class="truncate" truncated #cutChip="truncated" [tooltip]="chip.text" [tooltipDisabled]="!cutChip.isTruncated()">{{ chip.text }}</span>
32190
+ </span>
32191
+ }
32192
+ </div>
32193
+ }
32194
+ } @else {
32195
+ <!-- The empty-state rule of the list features: the feature's icon on a coloured tile, a title, one hint. -->
32196
+ <div class="flex flex-col items-center gap-(--spacing-xs) px-(--spacing-md) py-(--spacing-lg) text-center">
32197
+ <span class="inline-flex size-12 items-center justify-center rounded-(--radius-md) bg-(--bg-cyan-base-alt) text-(--font-cyan-muted)" aria-hidden="true">
32198
+ <quick-filter-icon class="size-6 opacity-80" />
32199
+ </span>
32200
+ <span class="text-(length:--size-base) font-semibold text-(--font-neutral-base)">{{ (query() ? "quickFilters.noMatch" : "quickFilters.empty") | transloco }}</span>
32201
+ @if (!query()) {
32202
+ <span class="text-(length:--size-sm) text-(--font-neutral-muted)">{{ emptyHint() ?? ("quickFilters.emptyHint" | transloco) }}</span>
32203
+ }
32204
+ </div>
32205
+ }
32206
+
32207
+ <span role="status" class="sr-only">{{ announcement() }}</span>
32208
+ `,
32209
+ isInline: true,
32210
+ dependencies: [
32211
+ {
32212
+ kind: "component",
32213
+ type: FaIconComponent,
32214
+ selector: "fa-icon, FaIcon",
32215
+ inputs: ["faClass", "class"]
32216
+ },
32217
+ {
32218
+ kind: "directive",
32219
+ type: ListComponent,
32220
+ selector: "List,list",
32221
+ inputs: [
32222
+ "class",
32223
+ "variant",
32224
+ "size"
32225
+ ],
32226
+ exportAs: ["list"]
32227
+ },
32228
+ {
32229
+ kind: "directive",
32230
+ type: ListItemComponent,
32231
+ selector: "ListItem,list-item,listitem",
32232
+ inputs: [
32233
+ "class",
32234
+ "variant",
32235
+ "size",
32236
+ "depth"
32237
+ ],
32238
+ exportAs: ["listItem"]
32239
+ },
32240
+ {
32241
+ kind: "component",
32242
+ type: ListItemActionComponent,
32243
+ selector: "ListItemAction,list-item-action,listitemaction",
32244
+ inputs: [
32245
+ "ariaLabel",
32246
+ "disabled",
32247
+ "keys",
32248
+ "shortcut",
32249
+ "description"
32250
+ ],
32251
+ exportAs: ["listItemAction"]
32252
+ },
32253
+ {
32254
+ kind: "directive",
32255
+ type: ListItemIconDirective,
32256
+ selector: "ListItemIcon,list-item-icon,listitemicon",
32257
+ inputs: ["class"]
32258
+ },
32259
+ {
32260
+ kind: "directive",
32261
+ type: ListItemLeftDirective,
32262
+ selector: "ListItemLeft,list-item-left,listitemleft",
32263
+ inputs: ["class"]
32264
+ },
32265
+ {
32266
+ kind: "component",
32267
+ type: ListItemTextComponent,
32268
+ selector: "ListItemText,list-item-text,listitemtext",
32269
+ inputs: ["class", "subtext"],
32270
+ exportAs: ["listItemText"]
32271
+ },
32272
+ {
32273
+ kind: "component",
32274
+ type: QuickFilterIcon,
32275
+ selector: "quick-filter, quick-filter-icon, QuickFilter, QuickFilterIcon",
32276
+ inputs: ["class"]
32277
+ },
32278
+ {
32279
+ kind: "component",
32280
+ type: SearchInputComponent,
32281
+ selector: "SearchInput:not([suggestions]), search-input:not([suggestions]), searchinput:not([suggestions])"
32282
+ },
32283
+ {
32284
+ kind: "directive",
32285
+ type: TooltipDirective,
32286
+ selector: "[tooltip]",
32287
+ inputs: [
32288
+ "tooltip",
32289
+ "tooltipPosition",
32290
+ "tooltipEvent",
32291
+ "showDelay",
32292
+ "hideDelay",
32293
+ "life",
32294
+ "tooltipOffset",
32295
+ "autoHide",
32296
+ "hideOnEscape",
32297
+ "escape",
32298
+ "tooltipDisabled",
32299
+ "tooltipArrow",
32300
+ "tooltipStyleClass",
32301
+ "tooltipOptions",
32302
+ "tooltip-position",
32303
+ "tooltip-delay",
32304
+ "tooltip-duration",
32305
+ "tooltip-offset"
32306
+ ],
32307
+ exportAs: ["tooltip"]
32308
+ },
32309
+ {
32310
+ kind: "directive",
32311
+ type: TruncatedDirective,
32312
+ selector: "[truncated]",
32313
+ inputs: ["truncated"],
32314
+ exportAs: ["truncated"]
32315
+ },
32316
+ {
32317
+ kind: "component",
32318
+ type: XMarkIcon,
32319
+ selector: "x-mark, x-mark-icon, XMark, XMarkIcon",
32320
+ inputs: ["class"]
32321
+ },
32322
+ {
32323
+ kind: "pipe",
32324
+ type: TranslocoPipe,
32325
+ name: "transloco"
32326
+ }
32327
+ ]
32328
+ });
32329
+ };
32330
+ i0.ɵɵngDeclareClassMetadata({
32331
+ minVersion: "12.0.0",
32332
+ version: "22.2.1",
32333
+ ngImport: i0,
32334
+ type: QuickFiltersListComponent,
32335
+ decorators: [{
32336
+ type: Component,
32337
+ args: [{
32338
+ selector: "QuickFiltersList, quick-filters-list",
32339
+ imports: [
32340
+ TranslocoPipe,
32341
+ FaIconComponent,
32342
+ ListComponent,
32343
+ ListItemComponent,
32344
+ ListItemActionComponent,
32345
+ ListItemIconDirective,
32346
+ ListItemLeftDirective,
32347
+ ListItemTextComponent,
32348
+ QuickFilterIcon,
32349
+ SearchInputComponent,
32350
+ TooltipDirective,
32351
+ TruncatedDirective,
32352
+ XMarkIcon
32353
+ ],
32354
+ providers: [provideTranslocoScope("quick-filters")],
32355
+ host: {
32356
+ class: "flex min-h-0 flex-col gap-(--spacing-xs) outline-none",
32357
+ tabindex: "-1",
32358
+ "(keydown.escape)": "onEscape($event)"
32359
+ },
32360
+ template: `
32361
+ @if (searchable() && quickFilters().length > 0) {
32362
+ <SearchInput
32363
+ size="md"
32364
+ class="border-(--stroke-primary-light)"
32365
+ [(value)]="query"
32366
+ [placeholder]="'quickFilters.search' | transloco"
32367
+ [aria-label]="'quickFilters.search' | transloco"
32368
+ [clearLabel]="'quickFilters.clearSearch' | transloco" />
32369
+ }
32370
+
32371
+ @if (filtered().length) {
32372
+ <!-- The galactik List, as the design's List Item: a listbox of menu rows with an icon, the name and the number of filters.
32373
+ Choosing a row (click, Enter) applies it; the selection is not kept. The cross at the end of the row deletes it. -->
32374
+ <List
32375
+ variant="list-menu"
32376
+ size="medium"
32377
+ selectionMode="explicit"
32378
+ class="-mx-1 min-h-0 flex-1 overflow-auto px-1 [scrollbar-color:color-mix(in_srgb,currentColor_40%,transparent)_transparent] [scrollbar-width:thin]"
32379
+ [attr.aria-label]="'quickFilters.list' | transloco"
32380
+ [value]="picked()"
32381
+ (valueChange)="pick($event)">
32382
+ @for (quickFilter of filtered(); track quickFilter.id; let index = $index) {
32383
+ <ListItem
32384
+ [value]="quickFilter"
32385
+ [label]="quickFilter.name"
32386
+ (focusin)="showDetails(quickFilter, count)"
32387
+ (focusout)="scheduleHideDetails()">
32388
+ <ListItemLeft>
32389
+ <ListItemIcon><quick-filter-icon /></ListItemIcon>
32390
+ <ListItemText>
32391
+ <!-- The tooltip shows the whole name, and only when it is cut off: a full name needs none. -->
32392
+ <span truncated #cut="truncated" [tooltip]="quickFilter.name" [tooltipDisabled]="!cut.isTruncated()" class="block truncate">{{ quickFilter.name }}</span>
32393
+ <!-- The line of the count of filters is the one that shows what the quick filter holds, on hover (the row itself only applies
32394
+ it); on the keyboard, the row having the focus shows it. It takes the look of the subtext of a row. -->
32395
+ <span
32396
+ #count
32397
+ data-count
32398
+ class="list-item-subtext block w-fit! decoration-dotted underline-offset-2 hover:underline"
32399
+ (mouseenter)="showDetails(quickFilter, count)"
32400
+ (mouseleave)="scheduleHideDetails()">
32401
+ {{ (quickFilter.filters.length === 1 ? "quickFilters.countOne" : "quickFilters.countOther") | transloco: { count: quickFilter.filters.length } }}
32402
+ </span>
32403
+ </ListItemText>
32404
+ </ListItemLeft>
32405
+ @if (deletable()) {
32406
+ <ListItemAction
32407
+ [ariaLabel]="'quickFilters.delete' | transloco: { name: quickFilter.name }"
32408
+ [keys]="deleteKeys"
32409
+ [shortcut]="'quickFilters.deleteShortcut' | transloco"
32410
+ [description]="'quickFilters.deleteDescription' | transloco: { key: 'quickFilters.deleteShortcut' | transloco }"
32411
+ (click)="remove(quickFilter, index, $event)">
32412
+ <x-mark-icon />
32413
+ </ListItemAction>
32414
+ }
32415
+ </ListItem>
32416
+ }
32417
+ </List>
32418
+
32419
+ <!-- The hover card of the design: the filters a quick filter holds, as chips, hanging under its row. Placed with Floating UI in
32420
+ the top layer, so it is above whatever clips the list. It stays while the pointer is on it, and while the focus is on the row. -->
32421
+ @if (details(); as shown) {
32422
+ <div
32423
+ #detailsCard
32424
+ role="tooltip"
32425
+ class="fixed m-0 flex max-w-72 flex-wrap gap-(--spacing-3xs) rounded-(--radius-lg) border border-solid border-(--stroke-neutral-white) bg-(--bg-neutral-white) p-(--spacing-md) shadow-(--shadow-2)"
32426
+ (mouseenter)="cancelHideDetails()"
32427
+ (mouseleave)="scheduleHideDetails()">
32428
+ @for (chip of chips(shown.quickFilter); track $index) {
32429
+ <span
32430
+ data-chip
32431
+ class="inline-flex max-w-full min-w-0 items-center gap-(--spacing-3xs) rounded-(--radius-rounded) bg-(--bg-neutral-base) py-(--spacing-4xs) pr-(--spacing-2xs) pl-(--spacing-2xs) text-(length:--size-sm) font-medium text-(--font-neutral-base)">
32432
+ <!-- the icon the application gives the aggregation in the filters custom JSON, as on the filter button -->
32433
+ @if (chip.icon) {
32434
+ <fa-icon [faClass]="chip.icon" class="shrink-0" />
32435
+ }
32436
+ <!-- A chip that is cut off shows its whole text in a tooltip; one that fits has none. -->
32437
+ <span class="truncate" truncated #cutChip="truncated" [tooltip]="chip.text" [tooltipDisabled]="!cutChip.isTruncated()">{{ chip.text }}</span>
32438
+ </span>
32439
+ }
32440
+ </div>
32441
+ }
32442
+ } @else {
32443
+ <!-- The empty-state rule of the list features: the feature's icon on a coloured tile, a title, one hint. -->
32444
+ <div class="flex flex-col items-center gap-(--spacing-xs) px-(--spacing-md) py-(--spacing-lg) text-center">
32445
+ <span class="inline-flex size-12 items-center justify-center rounded-(--radius-md) bg-(--bg-cyan-base-alt) text-(--font-cyan-muted)" aria-hidden="true">
32446
+ <quick-filter-icon class="size-6 opacity-80" />
32447
+ </span>
32448
+ <span class="text-(length:--size-base) font-semibold text-(--font-neutral-base)">{{ (query() ? "quickFilters.noMatch" : "quickFilters.empty") | transloco }}</span>
32449
+ @if (!query()) {
32450
+ <span class="text-(length:--size-sm) text-(--font-neutral-muted)">{{ emptyHint() ?? ("quickFilters.emptyHint" | transloco) }}</span>
32451
+ }
32452
+ </div>
32453
+ }
32454
+
32455
+ <span role="status" class="sr-only">{{ announcement() }}</span>
32456
+ `
32457
+ }]
32458
+ }],
32459
+ ctorParameters: () => [],
32460
+ propDecorators: {
32461
+ searchable: [{
32462
+ type: i0.Input,
32463
+ args: [{
32464
+ isSignal: true,
32465
+ alias: "searchable",
32466
+ required: false
32467
+ }]
32468
+ }],
32469
+ deletable: [{
32470
+ type: i0.Input,
32471
+ args: [{
32472
+ isSignal: true,
32473
+ alias: "deletable",
32474
+ required: false
32475
+ }]
32476
+ }],
32477
+ emptyHint: [{
32478
+ type: i0.Input,
32479
+ args: [{
32480
+ isSignal: true,
32481
+ alias: "emptyHint",
32482
+ required: false
32483
+ }]
32484
+ }],
32485
+ applied: [{
32486
+ type: i0.Output,
32487
+ args: ["applied"]
32488
+ }],
32489
+ deleted: [{
32490
+ type: i0.Output,
32491
+ args: ["deleted"]
32492
+ }],
32493
+ detailsCard: [{
32494
+ type: i0.ViewChild,
32495
+ args: ["detailsCard", { isSignal: true }]
32496
+ }]
32497
+ }
32498
+ });
32499
+ let nextPanelId = 0;
32500
+ var FiltersPanelComponent = class FiltersPanelComponent {
32501
+ documentCount = input(0, ...ngDevMode ? [{ debugName: "documentCount" }] : /* istanbul ignore next */ []);
32502
+ closed = output();
32503
+ appStore = inject(AppStore);
32504
+ aggregationsStore = inject(AggregationsStore);
32505
+ queryParamsStore = inject(QueryParamsStore);
32506
+ userSettingsStore = inject(UserSettingsStore);
32507
+ transloco = inject(TranslocoService);
32508
+ injector = inject(Injector);
32509
+ nameId = `filters-panel-quick-filter-name-${nextPanelId++}`;
32510
+ tab = signal("all", ...ngDevMode ? [{ debugName: "tab" }] : /* istanbul ignore next */ []);
32511
+ query = signal("", ...ngDevMode ? [{ debugName: "query" }] : /* istanbul ignore next */ []);
32512
+ saving = signal(false, ...ngDevMode ? [{ debugName: "saving" }] : /* istanbul ignore next */ []);
32513
+ announcement = signal("", ...ngDevMode ? [{ debugName: "announcement" }] : /* istanbul ignore next */ []);
32514
+ name = signal("", ...ngDevMode ? [{ debugName: "name" }] : /* istanbul ignore next */ []);
32515
+ appliedCount = computed(() => this.queryParamsStore.filters().flatMap((filter) => this.leaves(filter)).reduce((count, leaf) => count + this.valuesOf(leaf), 0), ...ngDevMode ? [{ debugName: "appliedCount" }] : /* istanbul ignore next */ []);
32516
+ nameInput = viewChild("nameInput", ...ngDevMode ? [{ debugName: "nameInput" }] : /* istanbul ignore next */ []);
32517
+ saveButton = viewChild("saveButton", ...ngDevMode ? [{ debugName: "saveButton" }] : /* istanbul ignore next */ []);
32518
+ matching = computed(() => {
32519
+ const needle = this.query().trim().toLocaleLowerCase();
32520
+ if (!needle) return [];
32521
+ const aside = this.appStore.filters().filter((f) => f.position === "left" || f.position === "both");
32522
+ return this.appStore.getAuthorized(aside).map((f) => this.aggregationsStore.getAggregation(f.name, "name") ?? this.aggregationsStore.getAggregation(f.column, "column")).filter((agg) => !!agg && `${agg.name} ${agg.column}`.toLocaleLowerCase().includes(needle)).map((agg) => agg.name);
32523
+ }, ...ngDevMode ? [{ debugName: "matching" }] : /* istanbul ignore next */ []);
32524
+ selectTab(values) {
32525
+ const next = values.at(-1);
32526
+ if (next) this.tab.set(next);
32527
+ }
32528
+ leaves(filter) {
32529
+ return filter.filters?.length ? filter.filters.flatMap((f) => this.leaves(f)) : [filter];
32530
+ }
32531
+ valuesOf(leaf) {
32532
+ if (leaf.values?.length) return leaf.values.length;
32533
+ return leaf.value !== void 0 && leaf.value !== null ? 1 : 0;
32534
+ }
32535
+ startSaving() {
32536
+ this.name.set("");
32537
+ this.saving.set(true);
32538
+ afterNextRender(() => this.nameInput()?.nativeElement.focus(), { injector: this.injector });
32539
+ }
32540
+ closeSaving() {
32541
+ this.saving.set(false);
32542
+ afterNextRender(() => this.saveButton()?.nativeElement.focus(), { injector: this.injector });
32543
+ }
32544
+ onSaveEscape(event) {
32545
+ event.stopPropagation();
32546
+ this.closeSaving();
32547
+ }
32548
+ async saveQuickFilter(event) {
32549
+ event.preventDefault();
32550
+ const saved = await this.userSettingsStore.addQuickFilter(this.name(), structuredClone(this.queryParamsStore.filters()));
32551
+ if (!saved) return;
32552
+ this.closeSaving();
32553
+ this.announcement.set(this.transloco.translate("filters.panelQuickFilterSaved", { name: saved.name }));
32554
+ }
32555
+ onQuickFilterApplied(quickFilter) {
32556
+ this.announcement.set(this.transloco.translate("filters.panelQuickFilterApplied", { name: quickFilter.name }));
32557
+ }
32558
+ static ɵfac = i0.ɵɵngDeclareFactory({
32559
+ minVersion: "12.0.0",
32560
+ version: "22.2.1",
32561
+ ngImport: i0,
32562
+ type: FiltersPanelComponent,
32563
+ deps: [],
32564
+ target: i0.ɵɵFactoryTarget.Component
32565
+ });
32566
+ static ɵcmp = i0.ɵɵngDeclareComponent({
32567
+ minVersion: "17.0.0",
32568
+ version: "22.2.1",
32569
+ type: FiltersPanelComponent,
32570
+ isStandalone: true,
32571
+ selector: "filters-panel, FiltersPanel, filterspanel",
32572
+ inputs: { documentCount: {
32573
+ classPropertyName: "documentCount",
32574
+ publicName: "documentCount",
32575
+ isSignal: true,
32576
+ isRequired: false,
32577
+ transformFunction: null
32578
+ } },
32579
+ outputs: { closed: "closed" },
32580
+ host: { classAttribute: "block h-full min-h-0" },
32581
+ providers: [provideTranslocoScope("filters")],
32582
+ viewQueries: [{
32583
+ propertyName: "nameInput",
32584
+ first: true,
32585
+ predicate: ["nameInput"],
32586
+ descendants: true,
32587
+ isSignal: true
32588
+ }, {
32589
+ propertyName: "saveButton",
32590
+ first: true,
32591
+ predicate: ["saveButton"],
32592
+ descendants: true,
32593
+ isSignal: true
32594
+ }],
32595
+ ngImport: i0,
32596
+ template: `
32597
+ <div class="flex h-full min-h-0 flex-col gap-(--spacing-xs)">
32598
+ <header class="flex items-center justify-between gap-(--spacing-xs)">
32599
+ <h2 class="flex items-center gap-(--spacing-2xs) text-(length:--size-lg) font-semibold">
32600
+ <filter-icon class="text-(length:--icon-sm) [&_svg]:fill-current" aria-hidden="true" />
32601
+ {{ "filters.panelTitle" | transloco }}
32602
+ </h2>
32603
+ <button variant="tertiary" size="sm" [iconOnly]="true" (click)="closed.emit()" [tooltip]="'filters.panelClose' | transloco" tooltipPosition="bottom">
32604
+ <x-mark-icon />
32605
+ <span class="sr-only">{{ "filters.panelClose" | transloco }}</span>
32606
+ </button>
32607
+ </header>
32608
+
32609
+ <!-- A two-way switch that must always have one side on: the group lets a click on the active item
32610
+ clear the selection, which would leave the panel with no content, so that case is ignored. -->
32611
+ <ToggleGroup class="w-full" [attr.aria-label]="'filters.panelViews' | transloco" [value]="[tab()]" (valueChange)="selectTab($event)">
32612
+ <Toggle class="flex-1 text-(length:--size-xs) font-semibold" value="all">{{ "filters.panelTabAll" | transloco }}</Toggle>
32613
+ <Toggle class="flex-1 text-(length:--size-xs) font-semibold" value="quick">{{ "filters.panelTabQuick" | transloco }}</Toggle>
32614
+ </ToggleGroup>
32615
+
32616
+ @if (tab() === "all") {
32617
+ <SearchInput
32618
+ size="md"
32619
+ class="[&>*]:h-8 [&>*]:rounded-full"
32620
+ [(value)]="query"
32621
+ [placeholder]="'filters.panelSearchPlaceholder' | transloco"
32622
+ [aria-label]="'filters.searchFilters' | transloco"
32623
+ [clearLabel]="'filters.clearSearch' | transloco" />
32624
+
32625
+ <!-- The bar is the anchor of the card: the card hangs under the button, as wide as the panel's content, and covers the facets
32626
+ below it until it is closed. -->
32627
+ <div class="relative flex h-8 items-center justify-between gap-(--spacing-xs) text-(length:--size-sm) text-(--font-primary-muted)">
32628
+ <span>{{ (documentCount() === 1 ? "filters.panelDocumentsOne" : "filters.panelDocumentsOther") | transloco: { count: documentCount() | localizedNumber } }}</span>
32629
+ <button
32630
+ #saveButton
32631
+ variant="secondary"
32632
+ size="sm"
32633
+ aria-haspopup="dialog"
32634
+ [attr.aria-expanded]="saving()"
32635
+ [disabled]="appliedCount() === 0"
32636
+ (click)="saving() ? closeSaving() : startSaving()">
32637
+ <quick-filter-icon />
32638
+ {{ "filters.panelSaveQuickFilter" | transloco }}
32639
+ </button>
32640
+
32641
+ @if (saving()) {
32642
+ <form
32643
+ role="dialog"
32644
+ [attr.aria-label]="'filters.panelSaveQuickFilter' | transloco"
32645
+ class="absolute inset-x-0 top-full z-10 mt-(--spacing-2xs) flex flex-col gap-(--spacing-sm) rounded-(--radius-lg) border border-(--stroke-neutral-white) bg-(--bg-neutral-white) p-(--spacing-md) text-(--font-neutral-black) shadow-(--shadow-2)"
32646
+ (submit)="saveQuickFilter($event)"
32647
+ (keydown.escape)="onSaveEscape($event)">
32648
+ <div class="flex flex-col gap-(--spacing-3xs)">
32649
+ <div class="flex items-center justify-between">
32650
+ <label [for]="nameId" class="text-(length:--size-sm) font-medium">{{ "filters.panelNameLabel" | transloco }}</label>
32651
+ <span class="rounded-(--radius-xs) bg-(--bg-neutral-base) px-1.5 py-px text-(length:--size-xs) text-(--font-neutral-muted)">{{ "filters.panelRequired" | transloco }}</span>
32652
+ </div>
32653
+ <input-group size="sm" class="w-full">
32654
+ <input
32655
+ #nameInput
32656
+ input-control
32657
+ [id]="nameId"
32658
+ type="text"
32659
+ autocomplete="off"
32660
+ required
32661
+ [placeholder]="'filters.panelQuickFilterName' | transloco"
32662
+ [value]="name()"
32663
+ (input)="name.set(nameInput.value)" />
32664
+ </input-group>
32665
+ </div>
32666
+ <div class="flex justify-end gap-(--spacing-2xs)">
32667
+ <button type="button" variant="secondary" size="sm" (click)="closeSaving()">{{ "filters.panelCancel" | transloco }}</button>
32668
+ <button type="submit" variant="primary" size="sm" [disabled]="!name().trim()">{{ "filters.panelSave" | transloco }}</button>
32669
+ </div>
32670
+ </form>
32671
+ }
32672
+ </div>
32673
+
32674
+ <!-- single outer scroll lives here; aside-filters' own overflow is neutralized so it is not a second, default-styled scroll container.
32675
+ relative, and it is load-bearing: a scroll container only clips the descendants it is a containing block for. The facets hold sr-only
32676
+ elements, which are position: absolute; with this div left static they escaped the clip, sat at their flow position far below the
32677
+ visible part of a long facet, and made the whole page scroll — a second scrollbar next to the facet's own. -->
32678
+ <div class="relative -mx-1 flex min-h-0 flex-1 scrollbar-thin flex-col overflow-y-auto px-1">
32679
+ @if (query() && matching().length === 0) {
32680
+ <p class="py-(--spacing-md) text-center text-(length:--size-sm) text-(--font-neutral-muted)">{{ "filters.noFilterMatch" | transloco }}</p>
32681
+ } @else {
32682
+ <aside-filters class="overflow-visible" [aggregations]="query() ? matching() : []" />
32683
+ }
32684
+ </div>
32685
+ } @else {
32686
+ <!-- The list of the quick filters pill, inline: it owns the rows, the hover card, the cross and the empty state. -->
32687
+ <quick-filters-list
32688
+ class="flex-1"
32689
+ [emptyHint]="'filters.panelNoQuickFiltersHint' | transloco"
32690
+ (applied)="onQuickFilterApplied($event)" />
32691
+ }
32692
+
32693
+ <span role="status" class="sr-only">{{ announcement() }}</span>
32694
+ </div>
32695
+ `,
32696
+ isInline: true,
32697
+ dependencies: [
32698
+ {
32699
+ kind: "component",
32700
+ type: AsideFiltersComponent,
32701
+ selector: "aside-filters, AsideFilters, asidefilters",
32702
+ inputs: [
32703
+ "class",
32704
+ "aggregations",
32705
+ "exclusive",
32706
+ "collapsible",
32707
+ "collapsed",
32708
+ "hideWhenEmpty"
32709
+ ]
32710
+ },
32711
+ {
32712
+ kind: "directive",
32713
+ type: ButtonComponent,
32714
+ selector: "button",
32715
+ inputs: [
32716
+ "class",
32717
+ "variant",
32718
+ "scheme",
32719
+ "iconOnly",
32720
+ "size"
32721
+ ]
32722
+ },
32723
+ {
32724
+ kind: "component",
32725
+ type: FilterIcon,
32726
+ selector: "filter, filter-icon, Filter, FilterIcon",
32727
+ inputs: ["class"]
32728
+ },
32729
+ {
32730
+ kind: "directive",
32731
+ type: InputComponent,
32732
+ selector: "input-group, inputgroup, InputGroup",
32733
+ inputs: [
32734
+ "class",
32735
+ "size",
32736
+ "scheme",
32737
+ "filled",
32738
+ "numbers",
32739
+ "autoHeight",
32740
+ "textSize"
32741
+ ]
32742
+ },
32743
+ {
32744
+ kind: "directive",
32745
+ type: InputControlDirective,
32746
+ selector: "input[input-control], input[inputControl]",
32747
+ inputs: ["class"]
32748
+ },
32749
+ {
32750
+ kind: "component",
32751
+ type: QuickFilterIcon,
32752
+ selector: "quick-filter, quick-filter-icon, QuickFilter, QuickFilterIcon",
32753
+ inputs: ["class"]
32754
+ },
32755
+ {
32756
+ kind: "component",
32757
+ type: QuickFiltersListComponent,
32758
+ selector: "QuickFiltersList, quick-filters-list",
32759
+ inputs: [
32760
+ "searchable",
32761
+ "deletable",
32762
+ "emptyHint"
32763
+ ],
32764
+ outputs: ["applied", "deleted"]
32765
+ },
32766
+ {
32767
+ kind: "component",
32768
+ type: SearchInputComponent,
32769
+ selector: "SearchInput:not([suggestions]), search-input:not([suggestions]), searchinput:not([suggestions])"
32770
+ },
32771
+ {
32772
+ kind: "directive",
32773
+ type: ToggleGroupComponent,
32774
+ selector: "ToggleGroup, toggle-group, togglegroup",
32775
+ inputs: [
32776
+ "class",
32777
+ "value",
32778
+ "multi"
32779
+ ],
32780
+ outputs: ["valueChange"],
32781
+ exportAs: ["toggleGroup"]
32782
+ },
32783
+ {
32784
+ kind: "directive",
32785
+ type: ToggleItemComponent,
32786
+ selector: "Toggle[value]",
32787
+ inputs: [
32788
+ "value",
32789
+ "class",
32790
+ "variant",
32791
+ "size",
32792
+ "iconOnly",
32793
+ "showIcon"
32794
+ ],
32795
+ exportAs: ["toggle"]
32796
+ },
32797
+ {
32798
+ kind: "directive",
32799
+ type: TooltipDirective,
32800
+ selector: "[tooltip]",
32801
+ inputs: [
32802
+ "tooltip",
32803
+ "tooltipPosition",
32804
+ "tooltipEvent",
32805
+ "showDelay",
32806
+ "hideDelay",
32807
+ "life",
32808
+ "tooltipOffset",
32809
+ "autoHide",
32810
+ "hideOnEscape",
32811
+ "escape",
32812
+ "tooltipDisabled",
32813
+ "tooltipArrow",
32814
+ "tooltipStyleClass",
32815
+ "tooltipOptions",
32816
+ "tooltip-position",
32817
+ "tooltip-delay",
32818
+ "tooltip-duration",
32819
+ "tooltip-offset"
32820
+ ],
32821
+ exportAs: ["tooltip"]
32822
+ },
32823
+ {
32824
+ kind: "component",
32825
+ type: XMarkIcon,
32826
+ selector: "x-mark, x-mark-icon, XMark, XMarkIcon",
32827
+ inputs: ["class"]
32828
+ },
32829
+ {
32830
+ kind: "pipe",
32831
+ type: TranslocoPipe,
32832
+ name: "transloco"
32833
+ },
32834
+ {
32835
+ kind: "pipe",
32836
+ type: LocalizedNumberPipe,
32837
+ name: "localizedNumber"
32838
+ }
32839
+ ]
32840
+ });
32841
+ };
32842
+ i0.ɵɵngDeclareClassMetadata({
32843
+ minVersion: "12.0.0",
32844
+ version: "22.2.1",
32845
+ ngImport: i0,
32846
+ type: FiltersPanelComponent,
32847
+ decorators: [{
32848
+ type: Component,
32849
+ args: [{
32850
+ selector: "filters-panel, FiltersPanel, filterspanel",
32851
+ imports: [
32852
+ TranslocoPipe,
32853
+ LocalizedNumberPipe,
32854
+ AsideFiltersComponent,
32855
+ ButtonComponent,
32856
+ FilterIcon,
32857
+ InputComponent,
32858
+ InputControlDirective,
32859
+ QuickFilterIcon,
32860
+ QuickFiltersListComponent,
32861
+ SearchInputComponent,
32862
+ ToggleGroupComponent,
32863
+ ToggleItemComponent,
32864
+ TooltipDirective,
32865
+ XMarkIcon
32866
+ ],
32867
+ providers: [provideTranslocoScope("filters")],
32868
+ host: { class: "block h-full min-h-0" },
32869
+ template: `
32870
+ <div class="flex h-full min-h-0 flex-col gap-(--spacing-xs)">
32871
+ <header class="flex items-center justify-between gap-(--spacing-xs)">
32872
+ <h2 class="flex items-center gap-(--spacing-2xs) text-(length:--size-lg) font-semibold">
32873
+ <filter-icon class="text-(length:--icon-sm) [&_svg]:fill-current" aria-hidden="true" />
32874
+ {{ "filters.panelTitle" | transloco }}
32875
+ </h2>
32876
+ <button variant="tertiary" size="sm" [iconOnly]="true" (click)="closed.emit()" [tooltip]="'filters.panelClose' | transloco" tooltipPosition="bottom">
32877
+ <x-mark-icon />
32878
+ <span class="sr-only">{{ "filters.panelClose" | transloco }}</span>
32879
+ </button>
32880
+ </header>
32881
+
32882
+ <!-- A two-way switch that must always have one side on: the group lets a click on the active item
32883
+ clear the selection, which would leave the panel with no content, so that case is ignored. -->
32884
+ <ToggleGroup class="w-full" [attr.aria-label]="'filters.panelViews' | transloco" [value]="[tab()]" (valueChange)="selectTab($event)">
32885
+ <Toggle class="flex-1 text-(length:--size-xs) font-semibold" value="all">{{ "filters.panelTabAll" | transloco }}</Toggle>
32886
+ <Toggle class="flex-1 text-(length:--size-xs) font-semibold" value="quick">{{ "filters.panelTabQuick" | transloco }}</Toggle>
32887
+ </ToggleGroup>
32888
+
32889
+ @if (tab() === "all") {
32890
+ <SearchInput
32891
+ size="md"
32892
+ class="[&>*]:h-8 [&>*]:rounded-full"
32893
+ [(value)]="query"
32894
+ [placeholder]="'filters.panelSearchPlaceholder' | transloco"
32895
+ [aria-label]="'filters.searchFilters' | transloco"
32896
+ [clearLabel]="'filters.clearSearch' | transloco" />
32897
+
32898
+ <!-- The bar is the anchor of the card: the card hangs under the button, as wide as the panel's content, and covers the facets
32899
+ below it until it is closed. -->
32900
+ <div class="relative flex h-8 items-center justify-between gap-(--spacing-xs) text-(length:--size-sm) text-(--font-primary-muted)">
32901
+ <span>{{ (documentCount() === 1 ? "filters.panelDocumentsOne" : "filters.panelDocumentsOther") | transloco: { count: documentCount() | localizedNumber } }}</span>
32902
+ <button
32903
+ #saveButton
32904
+ variant="secondary"
32905
+ size="sm"
32906
+ aria-haspopup="dialog"
32907
+ [attr.aria-expanded]="saving()"
32908
+ [disabled]="appliedCount() === 0"
32909
+ (click)="saving() ? closeSaving() : startSaving()">
32910
+ <quick-filter-icon />
32911
+ {{ "filters.panelSaveQuickFilter" | transloco }}
32912
+ </button>
32913
+
32914
+ @if (saving()) {
32915
+ <form
32916
+ role="dialog"
32917
+ [attr.aria-label]="'filters.panelSaveQuickFilter' | transloco"
32918
+ class="absolute inset-x-0 top-full z-10 mt-(--spacing-2xs) flex flex-col gap-(--spacing-sm) rounded-(--radius-lg) border border-(--stroke-neutral-white) bg-(--bg-neutral-white) p-(--spacing-md) text-(--font-neutral-black) shadow-(--shadow-2)"
32919
+ (submit)="saveQuickFilter($event)"
32920
+ (keydown.escape)="onSaveEscape($event)">
32921
+ <div class="flex flex-col gap-(--spacing-3xs)">
32922
+ <div class="flex items-center justify-between">
32923
+ <label [for]="nameId" class="text-(length:--size-sm) font-medium">{{ "filters.panelNameLabel" | transloco }}</label>
32924
+ <span class="rounded-(--radius-xs) bg-(--bg-neutral-base) px-1.5 py-px text-(length:--size-xs) text-(--font-neutral-muted)">{{ "filters.panelRequired" | transloco }}</span>
32925
+ </div>
32926
+ <input-group size="sm" class="w-full">
32927
+ <input
32928
+ #nameInput
32929
+ input-control
32930
+ [id]="nameId"
32931
+ type="text"
32932
+ autocomplete="off"
32933
+ required
32934
+ [placeholder]="'filters.panelQuickFilterName' | transloco"
32935
+ [value]="name()"
32936
+ (input)="name.set(nameInput.value)" />
32937
+ </input-group>
32938
+ </div>
32939
+ <div class="flex justify-end gap-(--spacing-2xs)">
32940
+ <button type="button" variant="secondary" size="sm" (click)="closeSaving()">{{ "filters.panelCancel" | transloco }}</button>
32941
+ <button type="submit" variant="primary" size="sm" [disabled]="!name().trim()">{{ "filters.panelSave" | transloco }}</button>
32942
+ </div>
32943
+ </form>
32944
+ }
32945
+ </div>
32946
+
32947
+ <!-- single outer scroll lives here; aside-filters' own overflow is neutralized so it is not a second, default-styled scroll container.
32948
+ relative, and it is load-bearing: a scroll container only clips the descendants it is a containing block for. The facets hold sr-only
32949
+ elements, which are position: absolute; with this div left static they escaped the clip, sat at their flow position far below the
32950
+ visible part of a long facet, and made the whole page scroll — a second scrollbar next to the facet's own. -->
32951
+ <div class="relative -mx-1 flex min-h-0 flex-1 scrollbar-thin flex-col overflow-y-auto px-1">
32952
+ @if (query() && matching().length === 0) {
32953
+ <p class="py-(--spacing-md) text-center text-(length:--size-sm) text-(--font-neutral-muted)">{{ "filters.noFilterMatch" | transloco }}</p>
32954
+ } @else {
32955
+ <aside-filters class="overflow-visible" [aggregations]="query() ? matching() : []" />
32956
+ }
32957
+ </div>
32958
+ } @else {
32959
+ <!-- The list of the quick filters pill, inline: it owns the rows, the hover card, the cross and the empty state. -->
32960
+ <quick-filters-list
32961
+ class="flex-1"
32962
+ [emptyHint]="'filters.panelNoQuickFiltersHint' | transloco"
32963
+ (applied)="onQuickFilterApplied($event)" />
32964
+ }
32965
+
32966
+ <span role="status" class="sr-only">{{ announcement() }}</span>
32967
+ </div>
32968
+ `
32969
+ }]
32970
+ }],
32971
+ propDecorators: {
32972
+ documentCount: [{
32973
+ type: i0.Input,
32974
+ args: [{
32975
+ isSignal: true,
32976
+ alias: "documentCount",
32977
+ required: false
32978
+ }]
32979
+ }],
32980
+ closed: [{
32981
+ type: i0.Output,
32982
+ args: ["closed"]
32983
+ }],
32984
+ nameInput: [{
32985
+ type: i0.ViewChild,
32986
+ args: ["nameInput", { isSignal: true }]
32987
+ }],
32988
+ saveButton: [{
32989
+ type: i0.ViewChild,
32990
+ args: ["saveButton", { isSignal: true }]
32991
+ }]
32992
+ }
32993
+ });
31622
32994
  var LabelService = class LabelService {
31623
32995
  appStore = inject(AppStore);
31624
32996
  client = inject(ATOMIC_CLIENT);
@@ -32901,43 +34273,21 @@ i0.ɵɵngDeclareClassMetadata({
32901
34273
  }
32902
34274
  });
32903
34275
  let popoverUid = 0;
32904
- function filterLabel(filter) {
32905
- return filter.name || filter.field || "";
32906
- }
32907
- function filterValue(filter) {
32908
- if (filter.filters?.length) return filter.filters.map((f) => filterLabel(f) ? `${filterLabel(f)}: ${filterValue(f)}` : filterValue(f)).join(", ");
32909
- if (filter.values?.length) return filter.values.join(", ");
32910
- return filter.display ?? filter.value ?? [filter.start, filter.end].filter((v) => v !== void 0 && v !== "").join(" – ");
32911
- }
32912
34276
  var QuickFiltersComponent = class QuickFiltersComponent {
32913
34277
  scheme = input("sage", ...ngDevMode ? [{ debugName: "scheme" }] : /* istanbul ignore next */ []);
32914
34278
  appearance = input("pill", ...ngDevMode ? [{ debugName: "appearance" }] : /* istanbul ignore next */ []);
32915
34279
  position = input("bottom-start", ...ngDevMode ? [{ debugName: "position" }] : /* istanbul ignore next */ []);
32916
34280
  offset = input(8, ...ngDevMode ? [{ debugName: "offset" }] : /* istanbul ignore next */ []);
32917
34281
  applied = output();
32918
- userSettingsStore = inject(UserSettingsStore);
32919
- queryParamsStore = inject(QueryParamsStore);
32920
- appStore = inject(AppStore);
32921
34282
  transloco = inject(TranslocoService);
32922
34283
  host = inject(ElementRef);
32923
34284
  popoverId = `quick-filters-popover-${popoverUid++}`;
32924
34285
  resized = signal(false, ...ngDevMode ? [{ debugName: "resized" }] : /* istanbul ignore next */ []);
32925
34286
  flippedAbove = signal(false, ...ngDevMode ? [{ debugName: "flippedAbove" }] : /* istanbul ignore next */ []);
32926
34287
  placement = computed(() => this.resized() && this.flippedAbove() ? this.position().replace("bottom", "top") : this.position(), ...ngDevMode ? [{ debugName: "placement" }] : /* istanbul ignore next */ []);
32927
- details = signal(null, ...ngDevMode ? [{ debugName: "details" }] : /* istanbul ignore next */ []);
32928
- detailsCard = viewChild("detailsCard", ...ngDevMode ? [{ debugName: "detailsCard" }] : /* istanbul ignore next */ []);
32929
- hideTimer;
32930
34288
  popover = viewChild(PopoverDirective, ...ngDevMode ? [{ debugName: "popover" }] : /* istanbul ignore next */ []);
32931
34289
  isOpen = computed(() => this.popover()?.state() === "open", ...ngDevMode ? [{ debugName: "isOpen" }] : /* istanbul ignore next */ []);
32932
34290
  constructor() {
32933
- injectFloatingPanel({
32934
- panel: () => this.detailsCard()?.nativeElement,
32935
- anchor: () => this.details()?.anchor,
32936
- placement: "bottom-start",
32937
- offset: 4,
32938
- topLayer: true
32939
- });
32940
- inject(DestroyRef).onDestroy(() => this.cancelHideDetails());
32941
34291
  effect((onCleanup) => {
32942
34292
  if (!this.resized()) return;
32943
34293
  const onScroll = (event) => {
@@ -32957,27 +34307,9 @@ var QuickFiltersComponent = class QuickFiltersComponent {
32957
34307
  selected: false,
32958
34308
  open: this.isOpen()
32959
34309
  }) : this.isOpen() ? "border border-(--stroke-neutral-dark)/50" : "", ...ngDevMode ? [{ debugName: "triggerClass" }] : /* istanbul ignore next */ []);
32960
- quickFilters = computed(() => this.userSettingsStore.quickFilters() ?? [], ...ngDevMode ? [{ debugName: "quickFilters" }] : /* istanbul ignore next */ []);
32961
- query = signal("", ...ngDevMode ? [{ debugName: "query" }] : /* istanbul ignore next */ []);
32962
- filtered = computed(() => {
32963
- const query = this.query().trim().toLowerCase();
32964
- return query ? this.quickFilters().filter((quickFilter) => quickFilter.name.toLowerCase().includes(query)) : this.quickFilters();
32965
- }, ...ngDevMode ? [{ debugName: "filtered" }] : /* istanbul ignore next */ []);
32966
34310
  announcement = signal("", ...ngDevMode ? [{ debugName: "announcement" }] : /* istanbul ignore next */ []);
32967
- chips(quickFilter) {
32968
- return quickFilter.filters.map((filter) => {
32969
- const icon = (filter.field ? this.appStore.getAggregationCustomization(filter.field, filter.name) : void 0)?.icon;
32970
- const value = filterValue(filter);
32971
- const label = filterLabel(filter);
32972
- return {
32973
- text: icon || !label || !value ? value || label : `${label}: ${value}`,
32974
- icon
32975
- };
32976
- }).filter((chip) => chip.text);
32977
- }
32978
34311
  onToggle() {
32979
34312
  if (this.popover()?.state() === "open") return;
32980
- this.reset();
32981
34313
  this.unpin();
32982
34314
  }
32983
34315
  unpin() {
@@ -32996,44 +34328,12 @@ var QuickFiltersComponent = class QuickFiltersComponent {
32996
34328
  if (popover && trigger && element) this.flippedAbove.set(element.getBoundingClientRect().bottom <= trigger.getBoundingClientRect().top + 1);
32997
34329
  this.resized.set(true);
32998
34330
  }
32999
- showDetails(quickFilter, event) {
33000
- this.cancelHideDetails();
33001
- this.details.set({
33002
- quickFilter,
33003
- anchor: event.currentTarget
33004
- });
33005
- }
33006
- scheduleHideDetails() {
33007
- this.cancelHideDetails();
33008
- this.hideTimer = setTimeout(() => this.details.set(null), 100);
33009
- }
33010
- cancelHideDetails() {
33011
- clearTimeout(this.hideTimer);
33012
- }
33013
- hideDetails() {
33014
- this.cancelHideDetails();
33015
- this.details.set(null);
33016
- }
33017
- reset() {
33018
- this.cancelHideDetails();
33019
- this.details.set(null);
33020
- this.query.set("");
33021
- }
33022
34331
  closeOnFocusLeave(event) {
33023
34332
  const next = event.relatedTarget;
33024
34333
  if (!next || !(next instanceof Node) || this.host.nativeElement.contains(next)) return;
33025
34334
  this.popover()?.close();
33026
34335
  }
33027
- picked = signal([], ...ngDevMode ? [{ debugName: "picked" }] : /* istanbul ignore next */ []);
33028
- pick(values) {
33029
- const quickFilter = values.at(-1);
33030
- this.picked.set([]);
33031
- if (quickFilter) this.apply(quickFilter);
33032
- }
33033
34336
  apply(quickFilter) {
33034
- this.cancelHideDetails();
33035
- this.details.set(null);
33036
- this.queryParamsStore.patch({ filters: structuredClone(quickFilter.filters) });
33037
34337
  this.popover()?.close();
33038
34338
  this.applied.emit(quickFilter);
33039
34339
  this.announcement.set(this.transloco.translate("quickFilters.applied", { name: quickFilter.name }));
@@ -33084,20 +34384,11 @@ var QuickFiltersComponent = class QuickFiltersComponent {
33084
34384
  },
33085
34385
  outputs: { applied: "applied" },
33086
34386
  host: {
33087
- listeners: {
33088
- "focusout": "closeOnFocusLeave($event)",
33089
- "keydown.escape": "hideDetails()"
33090
- },
34387
+ listeners: { "focusout": "closeOnFocusLeave($event)" },
33091
34388
  classAttribute: "flex rounded-(--radius-2xl)"
33092
34389
  },
33093
34390
  providers: [provideTranslocoScope("quick-filters")],
33094
34391
  viewQueries: [{
33095
- propertyName: "detailsCard",
33096
- first: true,
33097
- predicate: ["detailsCard"],
33098
- descendants: true,
33099
- isSignal: true
33100
- }, {
33101
34392
  propertyName: "popover",
33102
34393
  first: true,
33103
34394
  predicate: PopoverDirective,
@@ -33137,87 +34428,7 @@ var QuickFiltersComponent = class QuickFiltersComponent {
33137
34428
  there is room under it. The list takes the height the popover is given. The popover has no padding above and under:
33138
34429
  the handles (12px each) are that space, and hug the edges. -->
33139
34430
  <PopoverResizeHandle edge="top" [label]="'quickFilters.resize' | transloco" (resizeStart)="onResizeStart()" />
33140
- <div class="flex min-h-0 flex-1 flex-col gap-(--spacing-xs)">
33141
- @if (quickFilters().length > 0) {
33142
- <SearchInput
33143
- size="md"
33144
- class="border-(--stroke-primary-light)"
33145
- [(value)]="query"
33146
- [placeholder]="'quickFilters.search' | transloco"
33147
- [aria-label]="'quickFilters.search' | transloco"
33148
- [clearLabel]="'quickFilters.clearSearch' | transloco" />
33149
- }
33150
-
33151
- @if (filtered().length) {
33152
- <!-- The galactik List, as the design's List Item: a listbox of menu rows with an icon, the name and the number of
33153
- filters. Choosing a row (click, Enter) applies it; the selection is not kept, the popover closes. Hovering a
33154
- row lists the filters it holds in a card. -->
33155
- <List
33156
- variant="list-menu"
33157
- size="medium"
33158
- selectionMode="explicit"
33159
- class="-mx-1 min-h-0 flex-1 overflow-auto px-1"
33160
- [attr.aria-label]="'quickFilters.list' | transloco"
33161
- [value]="picked()"
33162
- (valueChange)="pick($event)">
33163
- @for (quickFilter of filtered(); track quickFilter.id) {
33164
- <ListItem
33165
- [value]="quickFilter"
33166
- [label]="quickFilter.name"
33167
- (mouseenter)="showDetails(quickFilter, $event)"
33168
- (mouseleave)="scheduleHideDetails()"
33169
- (focusin)="showDetails(quickFilter, $event)"
33170
- (focusout)="scheduleHideDetails()">
33171
- <ListItemLeft>
33172
- <ListItemIcon><quick-filter-icon /></ListItemIcon>
33173
- <ListItemText
33174
- [subtext]="(quickFilter.filters.length === 1 ? 'quickFilters.countOne' : 'quickFilters.countOther') | transloco: { count: quickFilter.filters.length }">
33175
- {{ quickFilter.name }}
33176
- </ListItemText>
33177
- </ListItemLeft>
33178
- </ListItem>
33179
- }
33180
- </List>
33181
-
33182
- <!-- The hover card of the design: the filters a quick filter holds, as chips, hanging under its row. Placed with
33183
- Floating UI like the popovers, in the top layer so it is above the popover it comes from. It stays while the
33184
- pointer is on it, and while the focus is on the row. -->
33185
- @if (details(); as shown) {
33186
- <div
33187
- #detailsCard
33188
- role="tooltip"
33189
- class="fixed m-0 flex max-w-72 flex-wrap gap-(--spacing-3xs) rounded-(--radius-lg) border border-solid border-(--stroke-neutral-white) bg-(--bg-neutral-white) p-(--spacing-md) shadow-(--shadow-2)"
33190
- (mouseenter)="cancelHideDetails()"
33191
- (mouseleave)="scheduleHideDetails()">
33192
- @for (chip of chips(shown.quickFilter); track $index) {
33193
- <span
33194
- data-chip
33195
- class="inline-flex items-center gap-(--spacing-3xs) rounded-(--radius-rounded) bg-(--bg-neutral-base) py-(--spacing-4xs) pr-(--spacing-2xs) pl-(--spacing-2xs) text-(length:--size-sm) font-medium text-(--font-neutral-base)">
33196
- <!-- the icon the application gives the aggregation in the filters custom JSON, as on the filter button -->
33197
- @if (chip.icon) {
33198
- <fa-icon [faClass]="chip.icon" class="shrink-0" />
33199
- }
33200
- {{ chip.text }}
33201
- </span>
33202
- }
33203
- </div>
33204
- }
33205
- } @else {
33206
- <!-- The empty-state rule of the list features: the feature's icon on a coloured tile, a title, one hint. -->
33207
- <div class="flex flex-col items-center gap-(--spacing-xs) px-(--spacing-md) py-(--spacing-lg) text-center">
33208
- <span class="inline-flex size-12 items-center justify-center rounded-(--radius-md) bg-(--bg-cyan-base-alt) text-(--font-cyan-muted)" aria-hidden="true">
33209
- <quick-filter-icon class="size-6 opacity-80" />
33210
- </span>
33211
- <span class="text-(length:--size-base) font-semibold text-(--font-neutral-base)">{{
33212
- (query() ? "quickFilters.noMatch" : "quickFilters.empty") | transloco
33213
- }}</span>
33214
- @if (!query()) {
33215
- <span class="text-(length:--size-sm) text-(--font-neutral-muted)">{{ "quickFilters.emptyHint" | transloco }}</span>
33216
- }
33217
- </div>
33218
- }
33219
-
33220
- </div>
34431
+ <quick-filters-list class="flex-1" [searchable]="true" (applied)="apply($event)" />
33221
34432
  <PopoverResizeHandle edge="bottom" [label]="'quickFilters.resize' | transloco" (resizeStart)="onResizeStart()" />
33222
34433
  }
33223
34434
  </div>
@@ -33226,12 +34437,6 @@ var QuickFiltersComponent = class QuickFiltersComponent {
33226
34437
  `,
33227
34438
  isInline: true,
33228
34439
  dependencies: [
33229
- {
33230
- kind: "component",
33231
- type: FaIconComponent,
33232
- selector: "fa-icon, FaIcon",
33233
- inputs: ["faClass", "class"]
33234
- },
33235
34440
  {
33236
34441
  kind: "directive",
33237
34442
  type: ButtonComponent,
@@ -33244,48 +34449,6 @@ var QuickFiltersComponent = class QuickFiltersComponent {
33244
34449
  "size"
33245
34450
  ]
33246
34451
  },
33247
- {
33248
- kind: "directive",
33249
- type: ListComponent,
33250
- selector: "List,list",
33251
- inputs: [
33252
- "class",
33253
- "variant",
33254
- "size"
33255
- ],
33256
- exportAs: ["list"]
33257
- },
33258
- {
33259
- kind: "directive",
33260
- type: ListItemComponent,
33261
- selector: "ListItem,list-item,listitem",
33262
- inputs: [
33263
- "class",
33264
- "variant",
33265
- "size",
33266
- "depth"
33267
- ],
33268
- exportAs: ["listItem"]
33269
- },
33270
- {
33271
- kind: "directive",
33272
- type: ListItemIconDirective,
33273
- selector: "ListItemIcon,list-item-icon,listitemicon",
33274
- inputs: ["class"]
33275
- },
33276
- {
33277
- kind: "directive",
33278
- type: ListItemLeftDirective,
33279
- selector: "ListItemLeft,list-item-left,listitemleft",
33280
- inputs: ["class"]
33281
- },
33282
- {
33283
- kind: "component",
33284
- type: ListItemTextComponent,
33285
- selector: "ListItemText,list-item-text,listitemtext",
33286
- inputs: ["class", "subtext"],
33287
- exportAs: ["listItemText"]
33288
- },
33289
34452
  {
33290
34453
  kind: "directive",
33291
34454
  type: PopoverDirective,
@@ -33314,17 +34477,23 @@ var QuickFiltersComponent = class QuickFiltersComponent {
33314
34477
  ],
33315
34478
  outputs: ["resizeStart"]
33316
34479
  },
33317
- {
33318
- kind: "component",
33319
- type: SearchInputComponent,
33320
- selector: "SearchInput:not([suggestions]), search-input:not([suggestions]), searchinput:not([suggestions])"
33321
- },
33322
34480
  {
33323
34481
  kind: "component",
33324
34482
  type: QuickFilterIcon,
33325
34483
  selector: "quick-filter, quick-filter-icon, QuickFilter, QuickFilterIcon",
33326
34484
  inputs: ["class"]
33327
34485
  },
34486
+ {
34487
+ kind: "component",
34488
+ type: QuickFiltersListComponent,
34489
+ selector: "QuickFiltersList, quick-filters-list",
34490
+ inputs: [
34491
+ "searchable",
34492
+ "deletable",
34493
+ "emptyHint"
34494
+ ],
34495
+ outputs: ["applied", "deleted"]
34496
+ },
33328
34497
  {
33329
34498
  kind: "pipe",
33330
34499
  type: TranslocoPipe,
@@ -33344,23 +34513,16 @@ i0.ɵɵngDeclareClassMetadata({
33344
34513
  selector: "QuickFilters, quick-filters",
33345
34514
  imports: [
33346
34515
  TranslocoPipe,
33347
- FaIconComponent,
33348
34516
  ButtonComponent,
33349
- ListComponent,
33350
- ListItemComponent,
33351
- ListItemIconDirective,
33352
- ListItemLeftDirective,
33353
- ListItemTextComponent,
33354
34517
  PopoverDirective,
33355
34518
  PopoverResizeHandleComponent,
33356
- SearchInputComponent,
33357
- QuickFilterIcon
34519
+ QuickFilterIcon,
34520
+ QuickFiltersListComponent
33358
34521
  ],
33359
34522
  providers: [provideTranslocoScope("quick-filters")],
33360
34523
  host: {
33361
34524
  class: "flex rounded-(--radius-2xl)",
33362
- "(focusout)": "closeOnFocusLeave($event)",
33363
- "(keydown.escape)": "hideDetails()"
34525
+ "(focusout)": "closeOnFocusLeave($event)"
33364
34526
  },
33365
34527
  template: `
33366
34528
  <button
@@ -33394,87 +34556,7 @@ i0.ɵɵngDeclareClassMetadata({
33394
34556
  there is room under it. The list takes the height the popover is given. The popover has no padding above and under:
33395
34557
  the handles (12px each) are that space, and hug the edges. -->
33396
34558
  <PopoverResizeHandle edge="top" [label]="'quickFilters.resize' | transloco" (resizeStart)="onResizeStart()" />
33397
- <div class="flex min-h-0 flex-1 flex-col gap-(--spacing-xs)">
33398
- @if (quickFilters().length > 0) {
33399
- <SearchInput
33400
- size="md"
33401
- class="border-(--stroke-primary-light)"
33402
- [(value)]="query"
33403
- [placeholder]="'quickFilters.search' | transloco"
33404
- [aria-label]="'quickFilters.search' | transloco"
33405
- [clearLabel]="'quickFilters.clearSearch' | transloco" />
33406
- }
33407
-
33408
- @if (filtered().length) {
33409
- <!-- The galactik List, as the design's List Item: a listbox of menu rows with an icon, the name and the number of
33410
- filters. Choosing a row (click, Enter) applies it; the selection is not kept, the popover closes. Hovering a
33411
- row lists the filters it holds in a card. -->
33412
- <List
33413
- variant="list-menu"
33414
- size="medium"
33415
- selectionMode="explicit"
33416
- class="-mx-1 min-h-0 flex-1 overflow-auto px-1"
33417
- [attr.aria-label]="'quickFilters.list' | transloco"
33418
- [value]="picked()"
33419
- (valueChange)="pick($event)">
33420
- @for (quickFilter of filtered(); track quickFilter.id) {
33421
- <ListItem
33422
- [value]="quickFilter"
33423
- [label]="quickFilter.name"
33424
- (mouseenter)="showDetails(quickFilter, $event)"
33425
- (mouseleave)="scheduleHideDetails()"
33426
- (focusin)="showDetails(quickFilter, $event)"
33427
- (focusout)="scheduleHideDetails()">
33428
- <ListItemLeft>
33429
- <ListItemIcon><quick-filter-icon /></ListItemIcon>
33430
- <ListItemText
33431
- [subtext]="(quickFilter.filters.length === 1 ? 'quickFilters.countOne' : 'quickFilters.countOther') | transloco: { count: quickFilter.filters.length }">
33432
- {{ quickFilter.name }}
33433
- </ListItemText>
33434
- </ListItemLeft>
33435
- </ListItem>
33436
- }
33437
- </List>
33438
-
33439
- <!-- The hover card of the design: the filters a quick filter holds, as chips, hanging under its row. Placed with
33440
- Floating UI like the popovers, in the top layer so it is above the popover it comes from. It stays while the
33441
- pointer is on it, and while the focus is on the row. -->
33442
- @if (details(); as shown) {
33443
- <div
33444
- #detailsCard
33445
- role="tooltip"
33446
- class="fixed m-0 flex max-w-72 flex-wrap gap-(--spacing-3xs) rounded-(--radius-lg) border border-solid border-(--stroke-neutral-white) bg-(--bg-neutral-white) p-(--spacing-md) shadow-(--shadow-2)"
33447
- (mouseenter)="cancelHideDetails()"
33448
- (mouseleave)="scheduleHideDetails()">
33449
- @for (chip of chips(shown.quickFilter); track $index) {
33450
- <span
33451
- data-chip
33452
- class="inline-flex items-center gap-(--spacing-3xs) rounded-(--radius-rounded) bg-(--bg-neutral-base) py-(--spacing-4xs) pr-(--spacing-2xs) pl-(--spacing-2xs) text-(length:--size-sm) font-medium text-(--font-neutral-base)">
33453
- <!-- the icon the application gives the aggregation in the filters custom JSON, as on the filter button -->
33454
- @if (chip.icon) {
33455
- <fa-icon [faClass]="chip.icon" class="shrink-0" />
33456
- }
33457
- {{ chip.text }}
33458
- </span>
33459
- }
33460
- </div>
33461
- }
33462
- } @else {
33463
- <!-- The empty-state rule of the list features: the feature's icon on a coloured tile, a title, one hint. -->
33464
- <div class="flex flex-col items-center gap-(--spacing-xs) px-(--spacing-md) py-(--spacing-lg) text-center">
33465
- <span class="inline-flex size-12 items-center justify-center rounded-(--radius-md) bg-(--bg-cyan-base-alt) text-(--font-cyan-muted)" aria-hidden="true">
33466
- <quick-filter-icon class="size-6 opacity-80" />
33467
- </span>
33468
- <span class="text-(length:--size-base) font-semibold text-(--font-neutral-base)">{{
33469
- (query() ? "quickFilters.noMatch" : "quickFilters.empty") | transloco
33470
- }}</span>
33471
- @if (!query()) {
33472
- <span class="text-(length:--size-sm) text-(--font-neutral-muted)">{{ "quickFilters.emptyHint" | transloco }}</span>
33473
- }
33474
- </div>
33475
- }
33476
-
33477
- </div>
34559
+ <quick-filters-list class="flex-1" [searchable]="true" (applied)="apply($event)" />
33478
34560
  <PopoverResizeHandle edge="bottom" [label]="'quickFilters.resize' | transloco" (resizeStart)="onResizeStart()" />
33479
34561
  }
33480
34562
  </div>
@@ -33521,10 +34603,6 @@ i0.ɵɵngDeclareClassMetadata({
33521
34603
  type: i0.Output,
33522
34604
  args: ["applied"]
33523
34605
  }],
33524
- detailsCard: [{
33525
- type: i0.ViewChild,
33526
- args: ["detailsCard", { isSignal: true }]
33527
- }],
33528
34606
  popover: [{
33529
34607
  type: i0.ViewChild,
33530
34608
  args: [i0.forwardRef(() => PopoverDirective), { isSignal: true }]
@@ -37783,6 +38861,6 @@ function provideSessionExpiry(options = {}) {
37783
38861
  const queryNameResolver = () => {
37784
38862
  return inject(AppStore).getDefaultQuery()?.name || "";
37785
38863
  };
37786
- export { AGGREGATIONS_NAMES, AGGREGATIONS_NAMES_PRESET_DEFAULT, APP_FEATURES, ATOMIC_CLIENT, ATOMIC_SCOPED_PROVIDERS, AddToCollection, AdvancedFiltersBodyComponent, AdvancedFiltersComponent, AdvancedSearch, AdvancedSearchTabs, AggregationComponent, AggregationDateComponent, AggregationDateRange, AggregationItemDirective, AggregationListComponent, AggregationPanelComponent, AggregationTreeComponent, AggregationsService, AggregationsStore, Alert, AlertDialog, AlertForm, AlertFormComponent, AlertsComponent, AppService, AppStore, ApplicationService, ApplicationStore, AppliedFiltersComponent, ArticleEntities, ArticleEntitiesTabPanel, ArticleExtracts, ArticleExtractsTabPanel, ArticleHighlights, ArticleLabels, ArticleLabelsTabPanel, ArticleMatchLocations, ArticleMatchLocationsTabPanel, ArticleSimilarDocuments, ArticleSimilarDocumentsTabPanel, AsideFiltersComponent, AuditFeedbackType, AuditService, AuthGuard, AuthPageComponent, AutocompleteService, BOOKMARKS_CONFIG, BOOKMARKS_OPTIONS, BackdropComponent, BackdropService, BookmarkButtonComponent, BookmarksComponent, COLLECTIONS_CONFIG, COLLECTIONS_OPTIONS, COMPONENTS_FOR_DOCUMENT_TYPE, ChangePasswordComponent, ChildMarkerDirective, CollectionsComponent, CollectionsDialog, ConverterSelectComponent, DeleteCollection, DeleteCollectionDialog, DidYouMeanComponent, DocumentLocatorComponent, EditLabels, ErrorComponent, Export, ExportDialog, ExportService, FILTERS_BREAKPOINT, FILTER_DATE_ALLOW_CUSTOM_RANGE, FORM_ALERT_CLASS, FORM_BARE_CLASS, FORM_ERROR_CLASS, FORM_FIELD_CLASS, FORM_FOOTER_CLASS, FORM_LABEL_CLASS, FORM_SURFACE_CLASS, FaIconComponent, FeatureFlags, FeatureFlagsDialogComponent, Feedback, FeedbackDialogComponent, FileSizePipe, FilterButtonComponent, FiltersBarComponent, FloatingToolbarComponent, ForgotPasswordComponent, HIGHLIGHTS, HeatmapComponent, HighlightWordPipe, HistoryPanelComponent, HistoryTitleMarqueeDirective, InfinityScrollDirective, InlineWorker, JsonMethodPluginService, KeyboardShortcutsDialog, KeyboardShortcutsDialogComponent, KeyboardShortcutsListComponent, KeyboardShortcutsService, LabelService, LabelsEditDialog, ListPanelItemDirective, LoadingComponent, MarkdownPipe, MarkdownRendererService, MetadataComponent, MissingTermsComponent, MoreButtonComponent, MoreComponent, MultiSelectLabelsComponent, MultiSelectionToolbarComponent, NON_SEARCHABLE_COLUMNS, NON_SEARCHABLE_DEFAULTS, NavbarTabsComponent, NavigationService, NoResultComponent, OpenArticleOnCtrlEnterDirective, OperatorPipe, OverflowItemDirective, OverflowManagerDirective, OverflowStopDirective, OverrideUser, OverrideUserDialogComponent, PREVIEW_ASSISTANT_COMPONENT, PREVIEW_CONFIG, PagerComponent, PreviewActionsComponent, PreviewContentComponent, PreviewHeaderComponent, PreviewNavigator, PreviewService, PrincipalService, PrincipalStore, QueryParamsStore, QueryService, QuickFiltersComponent, RECENT_SEARCHES_CONFIG, RECENT_SEARCHES_OPTIONS, ROUTE_COMPONENTS, RecentSearchesComponent, ResetUserSettings, ResetUserSettingsDialogComponent, SAVED_SEARCHES_CONFIG, SAVED_SEARCHES_OPTIONS, SEARCH_BOX_CATEGORIES_SORT, SEARCH_BOX_CATEGORY_ICONS, STARTUP_DATA_SOURCE, SavedSearch, SavedSearchDialog, SavedSearchPopover, SavedSearchesComponent, SavedSearchesService, SearchBoxComponent, SearchFeedbackComponent, SearchInputFooter, SearchService, SelectArticleDirective, SelectionService, SelectionStore, Settings, SettingsAccountPanelComponent, SettingsAlertsPanelComponent, SettingsAppearancePanelComponent, SettingsDialogComponent, SettingsShortcutsPanelComponent, ShortcutDirective, ShowBookmarkDirective, SignInComponent, SignedOutComponent, SortSelectorComponent, SourceComponent, SourceIconPipe, SponsoredResultsComponent, SyslangPipe, THEMES, TagCloudComponent, TextChunkService, ThemeProviderDirective, ThemeSelectorComponent, ThemeStore, ThemeToggleComponent, TranslocoDateImpurePipe, UserOverrideService, UserProfileFormComponent, UserProfileService, UserSettingsPersistence, UserSettingsStore, WEIGHT_STEPS, WordCloudComponent, ZoomControlsComponent, __resetSignInMutexForTests, activateShortcutTarget, applyThemeToNativeElement, atomicClientProvider, auditInterceptorFn, authInterceptorFn, bodyInterceptorFn, bootstrapApp, buildHeatmap, buildQuery, buildTagCloud, candidatesFor, canonicalize, debouncedSignal, errorInterceptorFn, fieldErrorKey, fieldShowsError, filterKey, filterLeaves, filterValues, findAggregation, flagsFromWeekdays, focusWhenReady, galactikPrimitive, galactikSemantic, getCurrentPath, getCurrentQueryName, getQueryNameFromRoute, groupSearchesByDate, injectAdvancedFilters, injectAtomicClient, injectRouteNavigation, injectUserSettingsPatch, isAuthRoute, isSignInInFlight, itemLabel, layoutWordCloud, legacyFilterToFilter, makeWeightScale, multiSelectionToolbarVariants, paletteColor, processCssVars, provideAtomicClient, provideAtomicNotifications, provideAtomicScope, provideDocumentLang, provideKeyboardShortcuts, provideSessionExpiry, provideStartupAggregation, quantileScale, queryNameResolver, rebuildWithout, searchItemKey, seriesPalette, signIn, themeColorNameToCssVariable, themeColorsToCssVariables, timelinePalette, toAriaKeyshortcuts, toDisplayParts, toPercentages, toSeries, toastInterceptorFn, weekdaysFromFlags, withAggregationsFeatures, withAlertsFeatures, withAlpha, withAppFeatures, withApplicationFeatures, withAssistantFeatures, withBasketsFeatures, withBookmarkFeatures, withBootstrapApp, withExtractsFeatures, withFavoriteSearchesFeatures, withFetch, withMultiSelectionFeatures, withQueryParamsFeatures, withQuickFiltersFeatures, withRecentSearchesFeatures, withSavedSearchesFeatures, withSelectionFeatures, withThemeBodyHook, withThemes, withThemesFeatures, withUserSettingsFeatures };
38864
+ export { AGGREGATIONS_NAMES, AGGREGATIONS_NAMES_PRESET_DEFAULT, APP_FEATURES, ATOMIC_CLIENT, ATOMIC_SCOPED_PROVIDERS, AddToCollection, AdvancedFiltersBodyComponent, AdvancedFiltersComponent, AdvancedSearch, AdvancedSearchTabs, AggregationComponent, AggregationDateComponent, AggregationDateRange, AggregationItemDirective, AggregationListComponent, AggregationPanelComponent, AggregationTreeComponent, AggregationsService, AggregationsStore, Alert, AlertDialog, AlertForm, AlertFormComponent, AlertsComponent, AppService, AppStore, ApplicationService, ApplicationStore, AppliedFiltersComponent, ArticleEntities, ArticleEntitiesTabPanel, ArticleExtracts, ArticleExtractsTabPanel, ArticleHighlights, ArticleLabels, ArticleLabelsTabPanel, ArticleMatchLocations, ArticleMatchLocationsTabPanel, ArticleSimilarDocuments, ArticleSimilarDocumentsTabPanel, AsideFiltersComponent, AuditFeedbackType, AuditService, AuthGuard, AuthPageComponent, AutocompleteService, BOOKMARKS_CONFIG, BOOKMARKS_OPTIONS, BackdropComponent, BackdropService, BookmarkButtonComponent, BookmarksComponent, COLLECTIONS_CONFIG, COLLECTIONS_OPTIONS, COMPONENTS_FOR_DOCUMENT_TYPE, ChangePasswordComponent, ChildMarkerDirective, CollectionsComponent, CollectionsDialog, ConverterSelectComponent, DeleteCollection, DeleteCollectionDialog, DidYouMeanComponent, DocumentLocatorComponent, EditLabels, ErrorComponent, Export, ExportDialog, ExportService, FILTERS_BREAKPOINT, FILTER_DATE_ALLOW_CUSTOM_RANGE, FORM_ALERT_CLASS, FORM_BARE_CLASS, FORM_ERROR_CLASS, FORM_FIELD_CLASS, FORM_FOOTER_CLASS, FORM_LABEL_CLASS, FORM_SURFACE_CLASS, FaIconComponent, FeatureFlags, FeatureFlagsDialogComponent, Feedback, FeedbackDialogComponent, FileSizePipe, FilterButtonComponent, FiltersBarComponent, FiltersPanelComponent, FloatingToolbarComponent, ForgotPasswordComponent, HIGHLIGHTS, HeatmapComponent, HighlightWordPipe, HistoryPanelComponent, HistoryTitleMarqueeDirective, InfinityScrollDirective, InlineWorker, JsonMethodPluginService, KeyboardShortcutsDialog, KeyboardShortcutsDialogComponent, KeyboardShortcutsListComponent, KeyboardShortcutsService, LabelService, LabelsEditDialog, ListPanelItemDirective, LoadingComponent, LocalizedNumberPipe, MarkdownPipe, MarkdownRendererService, MetadataComponent, MissingTermsComponent, MoreButtonComponent, MoreComponent, MultiSelectLabelsComponent, MultiSelectionToolbarComponent, NON_SEARCHABLE_COLUMNS, NON_SEARCHABLE_DEFAULTS, NavbarTabsComponent, NavigationService, NoResultComponent, OpenArticleOnCtrlEnterDirective, OperatorPipe, OverflowItemDirective, OverflowManagerDirective, OverflowStopDirective, OverrideUser, OverrideUserDialogComponent, PREVIEW_ASSISTANT_COMPONENT, PREVIEW_CONFIG, PagerComponent, PreviewActionsComponent, PreviewContentComponent, PreviewHeaderComponent, PreviewNavigator, PreviewService, PrincipalService, PrincipalStore, QueryParamsStore, QueryService, QuickFiltersComponent, QuickFiltersListComponent, RECENT_SEARCHES_CONFIG, RECENT_SEARCHES_OPTIONS, ROUTE_COMPONENTS, RecentSearchesComponent, ResetUserSettings, ResetUserSettingsDialogComponent, SAVED_SEARCHES_CONFIG, SAVED_SEARCHES_OPTIONS, SEARCH_BOX_CATEGORIES_SORT, SEARCH_BOX_CATEGORY_ICONS, STARTUP_DATA_SOURCE, SavedSearch, SavedSearchDialog, SavedSearchPopover, SavedSearchesComponent, SavedSearchesService, SearchBoxComponent, SearchFeedbackComponent, SearchInputFooter, SearchService, SelectArticleDirective, SelectionService, SelectionStore, Settings, SettingsAccountPanelComponent, SettingsAlertsPanelComponent, SettingsAppearancePanelComponent, SettingsDialogComponent, SettingsShortcutsPanelComponent, ShortcutDirective, ShowBookmarkDirective, SignInComponent, SignedOutComponent, SortSelectorComponent, SourceComponent, SourceIconPipe, SponsoredResultsComponent, SyslangPipe, THEMES, TagCloudComponent, TextChunkService, ThemeProviderDirective, ThemeSelectorComponent, ThemeStore, ThemeToggleComponent, TranslocoDateImpurePipe, UserOverrideService, UserProfileFormComponent, UserProfileService, UserSettingsPersistence, UserSettingsStore, WEIGHT_STEPS, WordCloudComponent, ZoomControlsComponent, __resetSignInMutexForTests, activateShortcutTarget, applyThemeToNativeElement, atomicClientProvider, auditInterceptorFn, authInterceptorFn, bodyInterceptorFn, bootstrapApp, buildHeatmap, buildQuery, buildTagCloud, candidatesFor, canonicalize, debouncedSignal, errorInterceptorFn, fieldErrorKey, fieldShowsError, filterKey, filterLeaves, filterValues, findAggregation, flagsFromWeekdays, focusWhenReady, galactikPrimitive, galactikSemantic, getCurrentPath, getCurrentQueryName, getQueryNameFromRoute, groupSearchesByDate, injectAdvancedFilters, injectAtomicClient, injectRouteNavigation, injectUserSettingsPatch, isAuthRoute, isSignInInFlight, itemLabel, layoutWordCloud, legacyFilterToFilter, makeWeightScale, multiSelectionToolbarVariants, paletteColor, processCssVars, provideAtomicClient, provideAtomicNotifications, provideAtomicScope, provideDocumentLang, provideKeyboardShortcuts, provideSessionExpiry, provideStartupAggregation, quantileScale, queryNameResolver, rebuildWithout, searchItemKey, seriesPalette, signIn, themeColorNameToCssVariable, themeColorsToCssVariables, timelinePalette, toAriaKeyshortcuts, toDisplayParts, toPercentages, toSeries, toastInterceptorFn, weekdaysFromFlags, withAggregationsFeatures, withAlertsFeatures, withAlpha, withAppFeatures, withApplicationFeatures, withAssistantFeatures, withBasketsFeatures, withBookmarkFeatures, withBootstrapApp, withExtractsFeatures, withFavoriteSearchesFeatures, withFetch, withMultiSelectionFeatures, withQueryParamsFeatures, withQuickFiltersFeatures, withRecentSearchesFeatures, withSavedSearchesFeatures, withSelectionFeatures, withThemeBodyHook, withThemes, withThemesFeatures, withUserSettingsFeatures };
37787
38865
 
37788
38866
  //# sourceMappingURL=sinequa-atomic-angular.mjs.map