@sinequa/atomic-angular 2.14.0 → 2.15.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, ViewContainerRef, 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, finalize, 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, ChevronUpIcon, CircleCheckIcon, ClockIcon, CollapseIcon, CommentIcon, CommentsIcon, DIALOG_REF, DesktopIcon, DialogBodyComponent, DialogComponent, DialogContentComponent, DialogDescriptionComponent, DialogFooterComponent, DialogHeaderComponent, DialogTitleComponent, DropdownDirective, EditIcon, EllipsisIcon, EllipsisVerticalIcon, ExpandIcon, EyeIcon, EyeSlashIcon, FileHTMLIcon, FileIcon, FileMagnifyingGlassIcon, FileOutputIcon, FileSpreadsheetIcon, FileTextIcon, FilesIcon, 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, LinkIcon, ListComponent, ListItemActionComponent, ListItemAvatarDirective, ListItemComponent, ListItemIconDirective, ListItemLeftDirective, ListItemTextComponent, LoadingCircleIcon, LockIcon, MagnifyingGlassIcon, MagnifyingGlassMinusIcon, MagnifyingGlassPlusIcon, MapPinIcon, MenuComponent, MenuContentComponent, MenuItemComponent, MenuTriggerDirective, MoonIcon, PaletteIcon, PanelRightIcon, 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, SwipeActionComponent, SwitchComponent, SyncIcon, TabComponent, TabListComponent, TabPanelComponent, TabsComponent, TagComponent, TagIcon, TagInputComponent, TagsIcon, TextareaComponent, TextareaControlDirective, ThumbsDownIcon, ThumbsUpIcon, ToggleGroupComponent, ToggleItemComponent, ToggleLeftIcon, ToggleRightIcon, TooltipDirective, TrashCanIcon, TrashIcon, TreeComponent, TreeItemComponent, TreeItemExpanderComponent, TreeItemGroupComponent, TriangleAlertIcon, TruncatedDirective, UndoIcon, UserIcon, UserSecretIcon, VideoIcon, XMarkIcon, createCallable, injectCallRef, injectFloatingPanel, injectKeyboardAvailable, 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, ChevronUpIcon, CircleCheckIcon, ClockIcon, CollapseIcon, CommentIcon, CommentsIcon, DIALOG_REF, DesktopIcon, DialogBodyComponent, DialogComponent, DialogContentComponent, DialogDescriptionComponent, DialogFooterComponent, DialogHeaderComponent, DialogTitleComponent, DropdownDirective, EditIcon, EllipsisIcon, EllipsisVerticalIcon, ExpandIcon, EyeIcon, EyeSlashIcon, FileHTMLIcon, FileIcon, FileMagnifyingGlassIcon, FileOutputIcon, FileSpreadsheetIcon, FileTextIcon, FilesIcon, FilterIcon, FlagEnglishIcon, FlagFrenchIcon, FlagGermanIcon, FlashlightIcon, FolderIcon, FolderOpenIcon, FrownIcon, GearIcon, GitForkIcon, GlobeIcon, GripVerticalIcon, HighlighterIcon, HistoryIcon, HomeIcon, ImageIcon, InboxIcon, InfoCircleIcon, InputComponent, InputControlDirective, InputIconLeftDirective, InputIconRightDirective, KbdComponent, KbdGroupComponent, KeyIcon, KeyboardIcon, LanguageIcon, LightbulbIcon, LightbulbSlashIcon, LinkComponent, LinkIcon, ListComponent, ListItemActionComponent, ListItemAvatarDirective, ListItemComponent, ListItemIconDirective, ListItemLeftDirective, ListItemTextComponent, LoadingCircleIcon, LockIcon, MagnifyingGlassIcon, MagnifyingGlassMinusIcon, MagnifyingGlassPlusIcon, MapPinIcon, MenuComponent, MenuContentComponent, MenuItemComponent, MenuTriggerDirective, MoonIcon, PaletteIcon, PanelRightIcon, 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, SwipeActionComponent, SwitchComponent, SyncIcon, TabComponent, TabListComponent, TabPanelComponent, TabsComponent, TagComponent, TagIcon, TagInputComponent, TagsIcon, TextareaComponent, TextareaControlDirective, ThumbsDownIcon, ThumbsUpIcon, ToggleGroupComponent, ToggleItemComponent, ToggleLeftIcon, ToggleRightIcon, TooltipDirective, TrashCanIcon, TrashIcon, TreeComponent, TreeItemComponent, TreeItemExpanderComponent, TreeItemGroupComponent, TriangleAlertIcon, TruncatedDirective, UndoIcon, UserIcon, UserSecretIcon, VideoIcon, XMarkIcon, createCallable, injectCallRef, injectFloatingPanel, injectKeyboardAvailable, 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";
@@ -14,7 +14,7 @@ import { cn } from "cn";
14
14
  import highlightWords from "highlight-words";
15
15
  import { rxResource, takeUntilDestroyed, toSignal } from "@angular/core/rxjs-interop";
16
16
  import { cva } from "class-variance-authority";
17
- import { FormField, FormRoot, applyEach, debounce, form, readonly, required, schema, submit, validate } from "@angular/forms/signals";
17
+ import { FormField, FormRoot, applyEach, debounce, form, maxLength, readonly, required, schema, submit, validate } from "@angular/forms/signals";
18
18
  import { injectVirtualizer } from "@tanstack/angular-virtual";
19
19
  import { catchError as catchError$1, filter as filter$2, map as map$1, switchMap as switchMap$1 } from "rxjs/operators";
20
20
  import { Tab } from "@angular/aria/tabs";
@@ -6686,6 +6686,799 @@ i0.ɵɵngDeclareClassMetadata({
6686
6686
  }
6687
6687
  });
6688
6688
  const AddToCollection = createCallable(CollectionsDialog);
6689
+ let nextId = 0;
6690
+ var AddToCollectionPanelComponent = class AddToCollectionPanelComponent {
6691
+ articles = input.required(...ngDevMode ? [{ debugName: "articles" }] : /* istanbul ignore next */ []);
6692
+ done = output();
6693
+ changed = output();
6694
+ closeRequested = output();
6695
+ userSettingsStore = inject(UserSettingsStore);
6696
+ injector = inject(Injector);
6697
+ titleId = `add-to-collection-title-${nextId}`;
6698
+ nameId = `add-to-collection-name-${nextId}`;
6699
+ errorId = `add-to-collection-error-${nextId++}`;
6700
+ scrollElement = viewChild("scrollElement", ...ngDevMode ? [{ debugName: "scrollElement" }] : /* istanbul ignore next */ []);
6701
+ searchInput = viewChild("searchInput", ...ngDevMode ? [{ debugName: "searchInput" }] : /* istanbul ignore next */ []);
6702
+ nameInput = viewChild("nameInput", ...ngDevMode ? [{ debugName: "nameInput" }] : /* istanbul ignore next */ []);
6703
+ collections = computed(() => this.userSettingsStore.baskets(), ...ngDevMode ? [{ debugName: "collections" }] : /* istanbul ignore next */ []);
6704
+ showSearch = computed(() => this.collections().length > 6, ...ngDevMode ? [{ debugName: "showSearch" }] : /* istanbul ignore next */ []);
6705
+ count = computed(() => this.ids().length, ...ngDevMode ? [{ debugName: "count" }] : /* istanbul ignore next */ []);
6706
+ ids = computed(() => Array.from(new Set(this.articles().map((a) => a.id).filter((id) => !!id))), ...ngDevMode ? [{ debugName: "ids" }] : /* istanbul ignore next */ []);
6707
+ searchText = signal("", ...ngDevMode ? [{ debugName: "searchText" }] : /* istanbul ignore next */ []);
6708
+ searchField = form(this.searchText);
6709
+ createdHere = signal([], ...ngDevMode ? [{ debugName: "createdHere" }] : /* istanbul ignore next */ []);
6710
+ filteredCollections = computed(() => {
6711
+ const query = this.searchText().trim().toLowerCase();
6712
+ const created = this.createdHere();
6713
+ const all = this.collections();
6714
+ const matching = query ? all.filter((c) => c.name.toLowerCase().includes(query)) : all;
6715
+ const rank = (c) => {
6716
+ const i = created.indexOf(c.name);
6717
+ return i < 0 ? created.length : i;
6718
+ };
6719
+ return created.length ? [...matching].sort((a, b) => rank(a) - rank(b)) : matching;
6720
+ }, ...ngDevMode ? [{ debugName: "filteredCollections" }] : /* istanbul ignore next */ []);
6721
+ status = signal(null, ...ngDevMode ? [{ debugName: "status" }] : /* istanbul ignore next */ []);
6722
+ creating = signal(false, ...ngDevMode ? [{ debugName: "creating" }] : /* istanbul ignore next */ []);
6723
+ createModel = signal({ name: "" }, ...ngDevMode ? [{ debugName: "createModel" }] : /* istanbul ignore next */ []);
6724
+ createForm = form(this.createModel, (path) => {
6725
+ maxLength(path.name, 50);
6726
+ validate(path.name, ({ value }) => {
6727
+ const name = value().trim();
6728
+ if (!name) return { kind: "nameRequired" };
6729
+ if (this.nameTaken(name)) return { kind: "nameExists" };
6730
+ });
6731
+ }, { submission: { action: async () => this.create() } });
6732
+ nameError = computed(() => {
6733
+ const field = this.createForm.name();
6734
+ const kinds = field.errors().map((e) => e.kind);
6735
+ if (kinds.includes("nameExists")) return "nameExists";
6736
+ if (kinds.includes("nameRequired") && field.touched()) return "nameRequired";
6737
+ return null;
6738
+ }, ...ngDevMode ? [{ debugName: "nameError" }] : /* istanbul ignore next */ []);
6739
+ virtualizer = injectVirtualizer(() => {
6740
+ const list = this.filteredCollections();
6741
+ return {
6742
+ count: list.length,
6743
+ estimateSize: (index) => this.coverageOf(list[index]) === "some" ? 52 : 40,
6744
+ getItemKey: (index) => list[index]?.name ?? index,
6745
+ scrollElement: this.scrollElement()
6746
+ };
6747
+ });
6748
+ doneTimer;
6749
+ constructor() {
6750
+ afterNextRender(() => this.focusFirst());
6751
+ inject(DestroyRef).onDestroy(() => clearTimeout(this.doneTimer));
6752
+ }
6753
+ coverageOf(collection) {
6754
+ if (!collection) return "none";
6755
+ const held = this.heldIn(collection);
6756
+ if (held === 0) return "none";
6757
+ return held === this.count() ? "all" : "some";
6758
+ }
6759
+ heldIn(collection) {
6760
+ const inCollection = new Set(collection.ids ?? []);
6761
+ return this.ids().filter((id) => inCollection.has(id)).length;
6762
+ }
6763
+ async toggle(collection) {
6764
+ const ids = this.ids();
6765
+ if (ids.length === 0) return;
6766
+ const removing = this.coverageOf(collection) === "all";
6767
+ const updated = {
6768
+ ...collection,
6769
+ ids: removing ? (collection.ids ?? []).filter((id) => !ids.includes(id)) : Array.from(/* @__PURE__ */ new Set([...collection.ids ?? [], ...ids]))
6770
+ };
6771
+ const index = this.userSettingsStore.baskets().findIndex((b) => b.name === collection.name);
6772
+ if (index < 0) return;
6773
+ try {
6774
+ await this.userSettingsStore.updateBasket(updated, index);
6775
+ } catch (e) {
6776
+ error("Error updating collection:", e);
6777
+ return;
6778
+ }
6779
+ this.changed.emit();
6780
+ if (removing) this.status.set({
6781
+ kind: "removed",
6782
+ name: collection.name
6783
+ });
6784
+ else this.confirmAdded(collection.name);
6785
+ }
6786
+ startCreation() {
6787
+ this.creating.set(true);
6788
+ afterNextRender(() => this.nameInput()?.nativeElement.focus(), { injector: this.injector });
6789
+ }
6790
+ cancelCreation(event) {
6791
+ if (this.collections().length === 0) return;
6792
+ event?.preventDefault();
6793
+ event?.stopPropagation();
6794
+ this.createModel.set({ name: "" });
6795
+ this.createForm().reset();
6796
+ this.creating.set(false);
6797
+ afterNextRender(() => this.focusFirst(), { injector: this.injector });
6798
+ }
6799
+ async create() {
6800
+ const name = this.createModel().name.trim();
6801
+ try {
6802
+ await this.userSettingsStore.createBasket({
6803
+ name,
6804
+ ids: []
6805
+ });
6806
+ } catch (e) {
6807
+ error("Error creating collection:", e);
6808
+ return;
6809
+ }
6810
+ this.createModel.set({ name: "" });
6811
+ this.createForm().reset();
6812
+ this.searchText.set("");
6813
+ this.createdHere.update((names) => [name, ...names.filter((n) => n !== name)]);
6814
+ this.creating.set(false);
6815
+ this.status.set({
6816
+ kind: "created",
6817
+ name
6818
+ });
6819
+ this.changed.emit();
6820
+ afterNextRender(() => {
6821
+ const list = this.scrollElement()?.nativeElement;
6822
+ if (!list) return;
6823
+ list.scrollTop = 0;
6824
+ list.querySelector("button")?.focus();
6825
+ }, { injector: this.injector });
6826
+ }
6827
+ confirmAdded(name) {
6828
+ this.status.set({
6829
+ kind: "added",
6830
+ name
6831
+ });
6832
+ clearTimeout(this.doneTimer);
6833
+ this.doneTimer = setTimeout(() => this.done.emit(name), 900);
6834
+ }
6835
+ nameTaken(name) {
6836
+ const lower = name.toLowerCase();
6837
+ return this.collections().some((c) => c.name.toLowerCase() === lower);
6838
+ }
6839
+ onHostToggle(event) {
6840
+ if (event.newState === "open") this.focusFirst();
6841
+ }
6842
+ focusFirst() {
6843
+ (this.nameInput()?.nativeElement ?? this.searchInput()?.nativeElement ?? this.scrollElement()?.nativeElement.querySelector("button"))?.focus();
6844
+ }
6845
+ static ɵfac = i0.ɵɵngDeclareFactory({
6846
+ minVersion: "12.0.0",
6847
+ version: "22.2.1",
6848
+ ngImport: i0,
6849
+ type: AddToCollectionPanelComponent,
6850
+ deps: [],
6851
+ target: i0.ɵɵFactoryTarget.Component
6852
+ });
6853
+ static ɵcmp = i0.ɵɵngDeclareComponent({
6854
+ minVersion: "17.0.0",
6855
+ version: "22.2.1",
6856
+ type: AddToCollectionPanelComponent,
6857
+ isStandalone: true,
6858
+ selector: "AddToCollectionPanel, add-to-collection-panel",
6859
+ inputs: { articles: {
6860
+ classPropertyName: "articles",
6861
+ publicName: "articles",
6862
+ isSignal: true,
6863
+ isRequired: true,
6864
+ transformFunction: null
6865
+ } },
6866
+ outputs: {
6867
+ done: "done",
6868
+ changed: "changed",
6869
+ closeRequested: "closeRequested"
6870
+ },
6871
+ host: {
6872
+ attributes: { "role": "dialog" },
6873
+ listeners: { "toggle": "onHostToggle($event)" },
6874
+ properties: { "attr.aria-labelledby": "titleId" },
6875
+ classAttribute: "flex w-80 max-w-[calc(100vw-2*var(--spacing-md))] flex-col overflow-hidden rounded-(--radius-xl) border border-(--stroke-neutral-white) bg-(--bg-neutral-white) text-(--font-neutral-black) shadow-(--shadow-3)"
6876
+ },
6877
+ providers: [provideTranslocoScope("collections")],
6878
+ viewQueries: [
6879
+ {
6880
+ propertyName: "scrollElement",
6881
+ first: true,
6882
+ predicate: ["scrollElement"],
6883
+ descendants: true,
6884
+ isSignal: true
6885
+ },
6886
+ {
6887
+ propertyName: "searchInput",
6888
+ first: true,
6889
+ predicate: ["searchInput"],
6890
+ descendants: true,
6891
+ isSignal: true
6892
+ },
6893
+ {
6894
+ propertyName: "nameInput",
6895
+ first: true,
6896
+ predicate: ["nameInput"],
6897
+ descendants: true,
6898
+ isSignal: true
6899
+ }
6900
+ ],
6901
+ ngImport: i0,
6902
+ template: `
6903
+ <div class="flex items-center gap-(--spacing-2xs) py-(--spacing-xs) ps-(--spacing-md) pe-(--spacing-xs)">
6904
+ @if (status()?.kind === 'added') {
6905
+ <circle-check-icon class="shrink-0 text-(--font-success-muted)" aria-hidden="true" />
6906
+ }
6907
+ <!-- The title is also the live region: the confirmation replaces it, and a change of an
6908
+ existing status node is what a screen reader announces. -->
6909
+ <p [id]="titleId" role="status" class="m-0 min-w-0 flex-1 text-(length:--size-base) leading-(--line-height-3)">
6910
+ @switch (status()?.kind) {
6911
+ @case ('added') {
6912
+ {{ 'collections.addedTo' | transloco: { name: status()!.name } }}
6913
+ }
6914
+ @case ('removed') {
6915
+ {{ 'collections.removedFrom' | transloco: { name: status()!.name } }}
6916
+ }
6917
+ @case ('created') {
6918
+ {{ 'collections.createdNamed' | transloco: { name: status()!.name } }}
6919
+ }
6920
+ @default {
6921
+ {{ 'collections.addDocuments' | transloco: { count: count() } }}
6922
+ }
6923
+ }
6924
+ </p>
6925
+ <button
6926
+ type="button"
6927
+ variant="none"
6928
+ class="inline-flex size-6 shrink-0 cursor-pointer items-center justify-center rounded-(--radius-xs) text-(--font-primary-muted) hover:bg-(--bg-primary-lighter) hover:text-(--font-primary-base) focus-visible:outline-2 focus-visible:outline-offset-0 focus-visible:outline-(--stroke-focus) focus-visible:outline-solid pointer-coarse:size-11"
6929
+ [attr.aria-label]="'collections.close' | transloco"
6930
+ [tooltip]="'collections.close' | transloco"
6931
+ (click)="closeRequested.emit()">
6932
+ <x-mark-icon aria-hidden="true" />
6933
+ </button>
6934
+ </div>
6935
+
6936
+ <div class="flex min-h-0 flex-col gap-(--spacing-2xs) border-t border-(--stroke-neutral-white) pt-(--spacing-xs) pb-(--spacing-2xs)">
6937
+ @if (collections().length === 0) {
6938
+ <div class="flex flex-col items-center gap-(--spacing-3xs) px-(--spacing-md) pt-(--spacing-2xs) text-center">
6939
+ <span class="mb-(--spacing-3xs) inline-flex size-10 items-center justify-center rounded-(--radius-md) bg-(--bg-indigo-base-alt) text-(--font-indigo-base)" aria-hidden="true">
6940
+ <inbox-icon />
6941
+ </span>
6942
+ <p class="m-0 text-(length:--size-base) font-semibold">{{ 'collections.noCollections' | transloco }}</p>
6943
+ <p class="m-0 text-(length:--size-sm) text-(--font-neutral-muted)">{{ 'collections.noCollectionsPanelHint' | transloco }}</p>
6944
+ </div>
6945
+ }
6946
+
6947
+ <!-- As in lexiQ, the name field opens at the top of the list, which stays on screen, dimmed and
6948
+ inert, until the collection is created or the creation cancelled: the user does not leave
6949
+ the list for another screen. -->
6950
+ @if (creating() || collections().length === 0) {
6951
+ <form [formRoot]="createForm" class="flex flex-col gap-(--spacing-3xs) px-(--spacing-xs)">
6952
+ <label [for]="nameId" class="px-(--spacing-3xs) text-(length:--size-sm) font-medium text-(--font-neutral-base)">
6953
+ {{ 'collections.collectionName' | transloco }}
6954
+ </label>
6955
+ <div class="flex items-center gap-(--spacing-3xs)">
6956
+ <input-group class="min-w-0 flex-1">
6957
+ <input
6958
+ #nameInput
6959
+ input-control
6960
+ type="text"
6961
+ autocomplete="off"
6962
+ spellcheck="false"
6963
+ [id]="nameId"
6964
+ [attr.placeholder]="'collections.collectionNamePlaceholder' | transloco"
6965
+ [formField]="createForm.name"
6966
+ [attr.aria-invalid]="nameError() ? 'true' : null"
6967
+ [attr.aria-describedby]="nameError() ? errorId : null"
6968
+ (keydown.escape)="cancelCreation($event)" />
6969
+ </input-group>
6970
+ @if (collections().length > 0) {
6971
+ <button
6972
+ type="button"
6973
+ variant="secondary"
6974
+ size="md"
6975
+ [iconOnly]="true"
6976
+ class="shrink-0 pointer-coarse:size-11"
6977
+ [attr.aria-label]="'collections.cancelCreation' | transloco"
6978
+ [tooltip]="'collections.cancelCreation' | transloco"
6979
+ (click)="cancelCreation()">
6980
+ <x-mark-icon aria-hidden="true" />
6981
+ </button>
6982
+ <button
6983
+ type="submit"
6984
+ size="md"
6985
+ [iconOnly]="true"
6986
+ class="shrink-0 pointer-coarse:size-11"
6987
+ [attr.aria-label]="'collections.createCollection' | transloco"
6988
+ [tooltip]="'collections.createCollection' | transloco">
6989
+ <check-icon aria-hidden="true" />
6990
+ </button>
6991
+ } @else {
6992
+ <button type="submit" size="md" class="shrink-0 pointer-coarse:h-11">{{ 'collections.create' | transloco }}</button>
6993
+ }
6994
+ </div>
6995
+ <!-- No line kept in reserve for the message: it would leave an empty gap above the list. -->
6996
+ @if (nameError(); as kind) {
6997
+ <p [id]="errorId" class="m-0 px-(--spacing-3xs) text-(length:--size-sm) text-(--font-error-base)">
6998
+ {{ 'collections.' + kind | transloco }}
6999
+ </p>
7000
+ }
7001
+ </form>
7002
+ } @else {
7003
+ @if (showSearch()) {
7004
+ <input-group class="mx-(--spacing-xs) min-w-0">
7005
+ <search-icon input-icon-left aria-hidden="true" />
7006
+ <input
7007
+ #searchInput
7008
+ input-control
7009
+ type="search"
7010
+ autocomplete="off"
7011
+ spellcheck="false"
7012
+ [attr.aria-label]="'collections.searchCollections' | transloco"
7013
+ [attr.placeholder]="'collections.searchCollections' | transloco"
7014
+ [formField]="searchField" />
7015
+ </input-group>
7016
+ }
7017
+
7018
+ <div class="flex items-center ps-(--spacing-md) pe-(--spacing-xs)">
7019
+ <span class="flex-1 text-(length:--size-base) font-medium">{{ 'collections.label' | transloco }}</span>
7020
+ <button type="button" variant="tertiary" size="sm" (click)="startCreation()">
7021
+ <plus-icon aria-hidden="true" />
7022
+ {{ 'collections.newCollection' | transloco }}
7023
+ </button>
7024
+ </div>
7025
+ }
7026
+
7027
+ @if (collections().length > 0) {
7028
+ <div
7029
+ #scrollElement
7030
+ role="group"
7031
+ [attr.aria-label]="'collections.label' | transloco"
7032
+ [attr.inert]="creating() ? '' : null"
7033
+ class="max-h-[min(15rem,var(--panel-max-h,15rem))] overflow-y-auto px-(--spacing-2xs) transition-opacity duration-150 [scrollbar-color:color-mix(in_srgb,currentColor_40%,transparent)_transparent] [scrollbar-width:thin] motion-reduce:transition-none"
7034
+ [class.opacity-40]="creating()">
7035
+ @if (filteredCollections().length) {
7036
+ <div class="relative w-full" [style.height.px]="virtualizer.getTotalSize()">
7037
+ @for (vItem of virtualizer.getVirtualItems(); track vItem.key) {
7038
+ @let collection = filteredCollections()[vItem.index];
7039
+ @let coverage = coverageOf(collection);
7040
+ <div
7041
+ class="absolute inset-x-0 flex items-center gap-(--spacing-2xs) rounded-(--radius-md) px-(--spacing-2xs) hover:bg-(--bg-primary-lightest) data-[fresh=true]:bg-(--bg-primary-lightest)"
7042
+ [attr.data-fresh]="status()?.name === collection.name && status()?.kind !== 'removed'"
7043
+ [style.height.px]="vItem.size"
7044
+ [style.transform]="'translateY(' + vItem.start + 'px)'">
7045
+ <span class="inline-flex size-6 shrink-0 items-center justify-center rounded-(--radius-xs) bg-(--bg-primary-lighter) text-(--font-primary-base)" aria-hidden="true">
7046
+ <folder-icon />
7047
+ </span>
7048
+ <span class="min-w-0 flex-1">
7049
+ <span class="block truncate text-(length:--size-base)">{{ collection.name }}</span>
7050
+ @if (coverage === 'some') {
7051
+ <span class="block truncate text-(length:--size-sm) text-(--font-neutral-muted)">
7052
+ {{ 'collections.alreadyHasSome' | transloco: { count: heldIn(collection), total: count() } }}
7053
+ </span>
7054
+ }
7055
+ </span>
7056
+ @if (coverage === 'all') {
7057
+ <button
7058
+ type="button"
7059
+ size="sm"
7060
+ [iconOnly]="true"
7061
+ class="shrink-0 focus-visible:-outline-offset-2 pointer-coarse:size-11"
7062
+ aria-pressed="true"
7063
+ [attr.aria-label]="'collections.removeFrom' | transloco: { name: collection.name }"
7064
+ (click)="toggle(collection)">
7065
+ <check-icon aria-hidden="true" />
7066
+ </button>
7067
+ } @else {
7068
+ <button
7069
+ type="button"
7070
+ variant="secondary"
7071
+ size="sm"
7072
+ [iconOnly]="true"
7073
+ class="shrink-0 bg-(--bg-neutral-white) focus-visible:-outline-offset-2 pointer-coarse:size-11"
7074
+ [attr.aria-pressed]="coverage === 'some' ? 'mixed' : 'false'"
7075
+ [attr.aria-label]="(coverage === 'some' ? 'collections.addMissingTo' : 'collections.addDocumentsTo') | transloco: { count: count(), name: collection.name }"
7076
+ (click)="toggle(collection)">
7077
+ <plus-icon aria-hidden="true" />
7078
+ </button>
7079
+ }
7080
+ </div>
7081
+ }
7082
+ </div>
7083
+ } @else {
7084
+ <p class="m-0 px-(--spacing-2xs) py-(--spacing-xs) text-(length:--size-sm) text-(--font-neutral-muted)">
7085
+ {{ 'collections.noMatch' | transloco }}
7086
+ </p>
7087
+ }
7088
+ </div>
7089
+ }
7090
+ </div>
7091
+ `,
7092
+ isInline: true,
7093
+ dependencies: [
7094
+ {
7095
+ kind: "directive",
7096
+ type: FormRoot,
7097
+ selector: "form[formRoot]",
7098
+ inputs: ["formRoot"]
7099
+ },
7100
+ {
7101
+ kind: "directive",
7102
+ type: FormField,
7103
+ selector: "[formField]",
7104
+ inputs: ["formField"],
7105
+ exportAs: ["formField"]
7106
+ },
7107
+ {
7108
+ kind: "directive",
7109
+ type: ButtonComponent,
7110
+ selector: "button",
7111
+ inputs: [
7112
+ "class",
7113
+ "variant",
7114
+ "scheme",
7115
+ "iconOnly",
7116
+ "size"
7117
+ ]
7118
+ },
7119
+ {
7120
+ kind: "directive",
7121
+ type: InputComponent,
7122
+ selector: "input-group, inputgroup, InputGroup",
7123
+ inputs: [
7124
+ "class",
7125
+ "size",
7126
+ "scheme",
7127
+ "filled",
7128
+ "numbers",
7129
+ "autoHeight",
7130
+ "textSize"
7131
+ ]
7132
+ },
7133
+ {
7134
+ kind: "directive",
7135
+ type: InputControlDirective,
7136
+ selector: "input[input-control], input[inputControl]",
7137
+ inputs: ["class"]
7138
+ },
7139
+ {
7140
+ kind: "directive",
7141
+ type: InputIconLeftDirective,
7142
+ selector: "[input-icon-left], [inputIconLeft]",
7143
+ inputs: ["class"]
7144
+ },
7145
+ {
7146
+ kind: "directive",
7147
+ type: TooltipDirective,
7148
+ selector: "[tooltip]",
7149
+ inputs: [
7150
+ "tooltip",
7151
+ "tooltipPosition",
7152
+ "tooltipEvent",
7153
+ "showDelay",
7154
+ "hideDelay",
7155
+ "life",
7156
+ "tooltipOffset",
7157
+ "autoHide",
7158
+ "hideOnEscape",
7159
+ "escape",
7160
+ "tooltipDisabled",
7161
+ "tooltipArrow",
7162
+ "tooltipStyleClass",
7163
+ "tooltipOptions",
7164
+ "tooltip-position",
7165
+ "tooltip-delay",
7166
+ "tooltip-duration",
7167
+ "tooltip-offset"
7168
+ ],
7169
+ exportAs: ["tooltip"]
7170
+ },
7171
+ {
7172
+ kind: "component",
7173
+ type: CheckIcon,
7174
+ selector: "check, check-icon, Check, CheckIcon",
7175
+ inputs: ["class"]
7176
+ },
7177
+ {
7178
+ kind: "component",
7179
+ type: CircleCheckIcon,
7180
+ selector: "circle-check, circle-check-icon, CircleCheck, CircleCheckIcon",
7181
+ inputs: ["class"]
7182
+ },
7183
+ {
7184
+ kind: "component",
7185
+ type: FolderIcon,
7186
+ selector: "folder, folder-icon, Folder, FolderIcon",
7187
+ inputs: ["class"]
7188
+ },
7189
+ {
7190
+ kind: "component",
7191
+ type: InboxIcon,
7192
+ selector: "inbox, inbox-icon, Inbox, InboxIcon",
7193
+ inputs: ["class"]
7194
+ },
7195
+ {
7196
+ kind: "component",
7197
+ type: PlusIcon,
7198
+ selector: "plus, plus-icon, Plus, PlusIcon",
7199
+ inputs: ["class"]
7200
+ },
7201
+ {
7202
+ kind: "component",
7203
+ type: SearchIcon,
7204
+ selector: "search, search-icon, Search, SearchIcon",
7205
+ inputs: ["class"]
7206
+ },
7207
+ {
7208
+ kind: "component",
7209
+ type: XMarkIcon,
7210
+ selector: "x-mark, x-mark-icon, XMark, XMarkIcon",
7211
+ inputs: ["class"]
7212
+ },
7213
+ {
7214
+ kind: "pipe",
7215
+ type: TranslocoPipe,
7216
+ name: "transloco"
7217
+ }
7218
+ ]
7219
+ });
7220
+ };
7221
+ i0.ɵɵngDeclareClassMetadata({
7222
+ minVersion: "12.0.0",
7223
+ version: "22.2.1",
7224
+ ngImport: i0,
7225
+ type: AddToCollectionPanelComponent,
7226
+ decorators: [{
7227
+ type: Component,
7228
+ args: [{
7229
+ selector: "AddToCollectionPanel, add-to-collection-panel",
7230
+ imports: [
7231
+ FormRoot,
7232
+ FormField,
7233
+ TranslocoPipe,
7234
+ ButtonComponent,
7235
+ InputComponent,
7236
+ InputControlDirective,
7237
+ InputIconLeftDirective,
7238
+ TooltipDirective,
7239
+ CheckIcon,
7240
+ CircleCheckIcon,
7241
+ FolderIcon,
7242
+ InboxIcon,
7243
+ PlusIcon,
7244
+ SearchIcon,
7245
+ XMarkIcon
7246
+ ],
7247
+ providers: [provideTranslocoScope("collections")],
7248
+ host: {
7249
+ role: "dialog",
7250
+ "[attr.aria-labelledby]": "titleId",
7251
+ "(toggle)": "onHostToggle($event)",
7252
+ class: "flex w-80 max-w-[calc(100vw-2*var(--spacing-md))] flex-col overflow-hidden rounded-(--radius-xl) border border-(--stroke-neutral-white) bg-(--bg-neutral-white) text-(--font-neutral-black) shadow-(--shadow-3)"
7253
+ },
7254
+ template: `
7255
+ <div class="flex items-center gap-(--spacing-2xs) py-(--spacing-xs) ps-(--spacing-md) pe-(--spacing-xs)">
7256
+ @if (status()?.kind === 'added') {
7257
+ <circle-check-icon class="shrink-0 text-(--font-success-muted)" aria-hidden="true" />
7258
+ }
7259
+ <!-- The title is also the live region: the confirmation replaces it, and a change of an
7260
+ existing status node is what a screen reader announces. -->
7261
+ <p [id]="titleId" role="status" class="m-0 min-w-0 flex-1 text-(length:--size-base) leading-(--line-height-3)">
7262
+ @switch (status()?.kind) {
7263
+ @case ('added') {
7264
+ {{ 'collections.addedTo' | transloco: { name: status()!.name } }}
7265
+ }
7266
+ @case ('removed') {
7267
+ {{ 'collections.removedFrom' | transloco: { name: status()!.name } }}
7268
+ }
7269
+ @case ('created') {
7270
+ {{ 'collections.createdNamed' | transloco: { name: status()!.name } }}
7271
+ }
7272
+ @default {
7273
+ {{ 'collections.addDocuments' | transloco: { count: count() } }}
7274
+ }
7275
+ }
7276
+ </p>
7277
+ <button
7278
+ type="button"
7279
+ variant="none"
7280
+ class="inline-flex size-6 shrink-0 cursor-pointer items-center justify-center rounded-(--radius-xs) text-(--font-primary-muted) hover:bg-(--bg-primary-lighter) hover:text-(--font-primary-base) focus-visible:outline-2 focus-visible:outline-offset-0 focus-visible:outline-(--stroke-focus) focus-visible:outline-solid pointer-coarse:size-11"
7281
+ [attr.aria-label]="'collections.close' | transloco"
7282
+ [tooltip]="'collections.close' | transloco"
7283
+ (click)="closeRequested.emit()">
7284
+ <x-mark-icon aria-hidden="true" />
7285
+ </button>
7286
+ </div>
7287
+
7288
+ <div class="flex min-h-0 flex-col gap-(--spacing-2xs) border-t border-(--stroke-neutral-white) pt-(--spacing-xs) pb-(--spacing-2xs)">
7289
+ @if (collections().length === 0) {
7290
+ <div class="flex flex-col items-center gap-(--spacing-3xs) px-(--spacing-md) pt-(--spacing-2xs) text-center">
7291
+ <span class="mb-(--spacing-3xs) inline-flex size-10 items-center justify-center rounded-(--radius-md) bg-(--bg-indigo-base-alt) text-(--font-indigo-base)" aria-hidden="true">
7292
+ <inbox-icon />
7293
+ </span>
7294
+ <p class="m-0 text-(length:--size-base) font-semibold">{{ 'collections.noCollections' | transloco }}</p>
7295
+ <p class="m-0 text-(length:--size-sm) text-(--font-neutral-muted)">{{ 'collections.noCollectionsPanelHint' | transloco }}</p>
7296
+ </div>
7297
+ }
7298
+
7299
+ <!-- As in lexiQ, the name field opens at the top of the list, which stays on screen, dimmed and
7300
+ inert, until the collection is created or the creation cancelled: the user does not leave
7301
+ the list for another screen. -->
7302
+ @if (creating() || collections().length === 0) {
7303
+ <form [formRoot]="createForm" class="flex flex-col gap-(--spacing-3xs) px-(--spacing-xs)">
7304
+ <label [for]="nameId" class="px-(--spacing-3xs) text-(length:--size-sm) font-medium text-(--font-neutral-base)">
7305
+ {{ 'collections.collectionName' | transloco }}
7306
+ </label>
7307
+ <div class="flex items-center gap-(--spacing-3xs)">
7308
+ <input-group class="min-w-0 flex-1">
7309
+ <input
7310
+ #nameInput
7311
+ input-control
7312
+ type="text"
7313
+ autocomplete="off"
7314
+ spellcheck="false"
7315
+ [id]="nameId"
7316
+ [attr.placeholder]="'collections.collectionNamePlaceholder' | transloco"
7317
+ [formField]="createForm.name"
7318
+ [attr.aria-invalid]="nameError() ? 'true' : null"
7319
+ [attr.aria-describedby]="nameError() ? errorId : null"
7320
+ (keydown.escape)="cancelCreation($event)" />
7321
+ </input-group>
7322
+ @if (collections().length > 0) {
7323
+ <button
7324
+ type="button"
7325
+ variant="secondary"
7326
+ size="md"
7327
+ [iconOnly]="true"
7328
+ class="shrink-0 pointer-coarse:size-11"
7329
+ [attr.aria-label]="'collections.cancelCreation' | transloco"
7330
+ [tooltip]="'collections.cancelCreation' | transloco"
7331
+ (click)="cancelCreation()">
7332
+ <x-mark-icon aria-hidden="true" />
7333
+ </button>
7334
+ <button
7335
+ type="submit"
7336
+ size="md"
7337
+ [iconOnly]="true"
7338
+ class="shrink-0 pointer-coarse:size-11"
7339
+ [attr.aria-label]="'collections.createCollection' | transloco"
7340
+ [tooltip]="'collections.createCollection' | transloco">
7341
+ <check-icon aria-hidden="true" />
7342
+ </button>
7343
+ } @else {
7344
+ <button type="submit" size="md" class="shrink-0 pointer-coarse:h-11">{{ 'collections.create' | transloco }}</button>
7345
+ }
7346
+ </div>
7347
+ <!-- No line kept in reserve for the message: it would leave an empty gap above the list. -->
7348
+ @if (nameError(); as kind) {
7349
+ <p [id]="errorId" class="m-0 px-(--spacing-3xs) text-(length:--size-sm) text-(--font-error-base)">
7350
+ {{ 'collections.' + kind | transloco }}
7351
+ </p>
7352
+ }
7353
+ </form>
7354
+ } @else {
7355
+ @if (showSearch()) {
7356
+ <input-group class="mx-(--spacing-xs) min-w-0">
7357
+ <search-icon input-icon-left aria-hidden="true" />
7358
+ <input
7359
+ #searchInput
7360
+ input-control
7361
+ type="search"
7362
+ autocomplete="off"
7363
+ spellcheck="false"
7364
+ [attr.aria-label]="'collections.searchCollections' | transloco"
7365
+ [attr.placeholder]="'collections.searchCollections' | transloco"
7366
+ [formField]="searchField" />
7367
+ </input-group>
7368
+ }
7369
+
7370
+ <div class="flex items-center ps-(--spacing-md) pe-(--spacing-xs)">
7371
+ <span class="flex-1 text-(length:--size-base) font-medium">{{ 'collections.label' | transloco }}</span>
7372
+ <button type="button" variant="tertiary" size="sm" (click)="startCreation()">
7373
+ <plus-icon aria-hidden="true" />
7374
+ {{ 'collections.newCollection' | transloco }}
7375
+ </button>
7376
+ </div>
7377
+ }
7378
+
7379
+ @if (collections().length > 0) {
7380
+ <div
7381
+ #scrollElement
7382
+ role="group"
7383
+ [attr.aria-label]="'collections.label' | transloco"
7384
+ [attr.inert]="creating() ? '' : null"
7385
+ class="max-h-[min(15rem,var(--panel-max-h,15rem))] overflow-y-auto px-(--spacing-2xs) transition-opacity duration-150 [scrollbar-color:color-mix(in_srgb,currentColor_40%,transparent)_transparent] [scrollbar-width:thin] motion-reduce:transition-none"
7386
+ [class.opacity-40]="creating()">
7387
+ @if (filteredCollections().length) {
7388
+ <div class="relative w-full" [style.height.px]="virtualizer.getTotalSize()">
7389
+ @for (vItem of virtualizer.getVirtualItems(); track vItem.key) {
7390
+ @let collection = filteredCollections()[vItem.index];
7391
+ @let coverage = coverageOf(collection);
7392
+ <div
7393
+ class="absolute inset-x-0 flex items-center gap-(--spacing-2xs) rounded-(--radius-md) px-(--spacing-2xs) hover:bg-(--bg-primary-lightest) data-[fresh=true]:bg-(--bg-primary-lightest)"
7394
+ [attr.data-fresh]="status()?.name === collection.name && status()?.kind !== 'removed'"
7395
+ [style.height.px]="vItem.size"
7396
+ [style.transform]="'translateY(' + vItem.start + 'px)'">
7397
+ <span class="inline-flex size-6 shrink-0 items-center justify-center rounded-(--radius-xs) bg-(--bg-primary-lighter) text-(--font-primary-base)" aria-hidden="true">
7398
+ <folder-icon />
7399
+ </span>
7400
+ <span class="min-w-0 flex-1">
7401
+ <span class="block truncate text-(length:--size-base)">{{ collection.name }}</span>
7402
+ @if (coverage === 'some') {
7403
+ <span class="block truncate text-(length:--size-sm) text-(--font-neutral-muted)">
7404
+ {{ 'collections.alreadyHasSome' | transloco: { count: heldIn(collection), total: count() } }}
7405
+ </span>
7406
+ }
7407
+ </span>
7408
+ @if (coverage === 'all') {
7409
+ <button
7410
+ type="button"
7411
+ size="sm"
7412
+ [iconOnly]="true"
7413
+ class="shrink-0 focus-visible:-outline-offset-2 pointer-coarse:size-11"
7414
+ aria-pressed="true"
7415
+ [attr.aria-label]="'collections.removeFrom' | transloco: { name: collection.name }"
7416
+ (click)="toggle(collection)">
7417
+ <check-icon aria-hidden="true" />
7418
+ </button>
7419
+ } @else {
7420
+ <button
7421
+ type="button"
7422
+ variant="secondary"
7423
+ size="sm"
7424
+ [iconOnly]="true"
7425
+ class="shrink-0 bg-(--bg-neutral-white) focus-visible:-outline-offset-2 pointer-coarse:size-11"
7426
+ [attr.aria-pressed]="coverage === 'some' ? 'mixed' : 'false'"
7427
+ [attr.aria-label]="(coverage === 'some' ? 'collections.addMissingTo' : 'collections.addDocumentsTo') | transloco: { count: count(), name: collection.name }"
7428
+ (click)="toggle(collection)">
7429
+ <plus-icon aria-hidden="true" />
7430
+ </button>
7431
+ }
7432
+ </div>
7433
+ }
7434
+ </div>
7435
+ } @else {
7436
+ <p class="m-0 px-(--spacing-2xs) py-(--spacing-xs) text-(length:--size-sm) text-(--font-neutral-muted)">
7437
+ {{ 'collections.noMatch' | transloco }}
7438
+ </p>
7439
+ }
7440
+ </div>
7441
+ }
7442
+ </div>
7443
+ `
7444
+ }]
7445
+ }],
7446
+ ctorParameters: () => [],
7447
+ propDecorators: {
7448
+ articles: [{
7449
+ type: i0.Input,
7450
+ args: [{
7451
+ isSignal: true,
7452
+ alias: "articles",
7453
+ required: true
7454
+ }]
7455
+ }],
7456
+ done: [{
7457
+ type: i0.Output,
7458
+ args: ["done"]
7459
+ }],
7460
+ changed: [{
7461
+ type: i0.Output,
7462
+ args: ["changed"]
7463
+ }],
7464
+ closeRequested: [{
7465
+ type: i0.Output,
7466
+ args: ["closeRequested"]
7467
+ }],
7468
+ scrollElement: [{
7469
+ type: i0.ViewChild,
7470
+ args: ["scrollElement", { isSignal: true }]
7471
+ }],
7472
+ searchInput: [{
7473
+ type: i0.ViewChild,
7474
+ args: ["searchInput", { isSignal: true }]
7475
+ }],
7476
+ nameInput: [{
7477
+ type: i0.ViewChild,
7478
+ args: ["nameInput", { isSignal: true }]
7479
+ }]
7480
+ }
7481
+ });
6689
7482
  var ExportService = class ExportService {
6690
7483
  client = inject(ATOMIC_CLIENT);
6691
7484
  download(options) {
@@ -7227,14 +8020,16 @@ i0.ɵɵngDeclareClassMetadata({
7227
8020
  }] }
7228
8021
  });
7229
8022
  const Export = createCallable(ExportDialog);
7230
- const multiSelectionToolbarVariants = cva("flex divide-x rounded-xl bg-linear-to-b px-3 py-2 shadow [&_ul]:flex [&_ul]:flex-col [&_ul]:p-1 [&>div]:px-2", {
8023
+ const multiSelectionToolbarVariants = cva("", {
7231
8024
  variants: { variant: {
7232
- dark: "divide-gray-800 bg-gray-900 from-gray-500 to-gray-900 text-white",
7233
- light: "divide-gray-400 bg-white from-white to-purple-50 text-gray-800",
7234
- glassy: "divide-gray-300 bg-white/70 from-transparent to-white text-gray-800 backdrop-blur-[15px]"
8025
+ dark: "flex divide-x rounded-xl bg-linear-to-b px-3 py-2 shadow [&_ul]:flex [&_ul]:flex-col [&_ul]:p-1 [&>div]:px-2 divide-gray-800 bg-gray-900 from-gray-500 to-gray-900 text-white",
8026
+ light: "flex divide-x rounded-xl bg-linear-to-b px-3 py-2 shadow [&_ul]:flex [&_ul]:flex-col [&_ul]:p-1 [&>div]:px-2 divide-gray-400 bg-white from-white to-purple-50 text-gray-800",
8027
+ glassy: "flex divide-x rounded-xl bg-linear-to-b px-3 py-2 shadow [&_ul]:flex [&_ul]:flex-col [&_ul]:p-1 [&>div]:px-2 divide-gray-300 bg-white/70 from-transparent to-white text-gray-800 backdrop-blur-[15px]",
8028
+ floating: "flex flex-wrap items-center justify-end gap-(--spacing-2xs)"
7235
8029
  } },
7236
8030
  defaultVariants: { variant: "dark" }
7237
8031
  });
8032
+ const FLOATING_BUTTON = "shadow-(--shadow-2) pointer-coarse:h-11";
7238
8033
  var MultiSelectionToolbarComponent = class MultiSelectionToolbarComponent {
7239
8034
  cn = cn;
7240
8035
  appStore = inject(AppStore);
@@ -7246,6 +8041,7 @@ var MultiSelectionToolbarComponent = class MultiSelectionToolbarComponent {
7246
8041
  hint = input(...ngDevMode ? [void 0, { debugName: "hint" }] : /* istanbul ignore next */ []);
7247
8042
  updatedCollections = output();
7248
8043
  cleared = output();
8044
+ addedToCollection = output();
7249
8045
  count = this.selection.multiSelectCount;
7250
8046
  variants = computed(() => cn(multiSelectionToolbarVariants({
7251
8047
  variant: this.variant(),
@@ -7265,6 +8061,25 @@ var MultiSelectionToolbarComponent = class MultiSelectionToolbarComponent {
7265
8061
  showAssistantAction = computed(() => this.allowAI() && this.appStore.isAssistantAllowed(this.instanceId()) && this.enabledUserInput(), ...ngDevMode ? [{ debugName: "showAssistantAction" }] : /* istanbul ignore next */ []);
7266
8062
  showHint = signal(false, ...ngDevMode ? [{ debugName: "showHint" }] : /* istanbul ignore next */ []);
7267
8063
  armed = true;
8064
+ floatingButton = FLOATING_BUTTON;
8065
+ hostClass = computed(() => {
8066
+ const active = this.count() > 0;
8067
+ if (this.variant() === "floating") return cn("fixed end-(--spacing-xl) bottom-(--spacing-xl) z-50 transition-[translate,opacity] duration-300 motion-reduce:transition-none", active ? "translate-y-0 opacity-100" : "pointer-events-none translate-y-2 opacity-0");
8068
+ return cn("fixed -bottom-20 left-[50%] z-50 translate-x-[-50%] transition-[translate,opacity,discrete] duration-300", active ? "-translate-y-24 opacity-100" : "opacity-0");
8069
+ }, ...ngDevMode ? [{ debugName: "hostClass" }] : /* istanbul ignore next */ []);
8070
+ toolbarClass = computed(() => this.variants(), ...ngDevMode ? [{ debugName: "toolbarClass" }] : /* istanbul ignore next */ []);
8071
+ static nextId = 0;
8072
+ panelId = `multi-selection-collections-${MultiSelectionToolbarComponent.nextId++}`;
8073
+ panelOpen = signal(false, ...ngDevMode ? [{ debugName: "panelOpen" }] : /* istanbul ignore next */ []);
8074
+ panelArticles = signal([], ...ngDevMode ? [{ debugName: "panelArticles" }] : /* istanbul ignore next */ []);
8075
+ lastFiled = signal(null, ...ngDevMode ? [{ debugName: "lastFiled" }] : /* istanbul ignore next */ []);
8076
+ collectionsChanged = false;
8077
+ collectionTrigger = viewChild("collectionTrigger", ...ngDevMode ? [{ debugName: "collectionTrigger" }] : /* istanbul ignore next */ []);
8078
+ panel = viewChild("panel", {
8079
+ ...ngDevMode ? { debugName: "panel" } : /* istanbul ignore next */ {},
8080
+ read: ElementRef
8081
+ });
8082
+ lastFiledTimer;
7268
8083
  constructor() {
7269
8084
  effect(() => {
7270
8085
  const count = this.count();
@@ -7275,9 +8090,21 @@ var MultiSelectionToolbarComponent = class MultiSelectionToolbarComponent {
7275
8090
  } else if (this.armed) {
7276
8091
  this.armed = false;
7277
8092
  this.showHint.set(true);
8093
+ this.lastFiled.set(null);
7278
8094
  } else this.showHint.set(false);
7279
8095
  });
7280
8096
  });
8097
+ effect(() => {
8098
+ if (this.count() === 0) untracked(() => this.panelOpen() && this.closePanel());
8099
+ });
8100
+ injectFloatingPanel({
8101
+ panel: () => this.panel()?.nativeElement,
8102
+ anchor: () => this.collectionTrigger()?.nativeElement,
8103
+ placement: "top-end",
8104
+ offset: 8,
8105
+ topLayer: true,
8106
+ maxHeightVariable: "--panel-max-h"
8107
+ });
7281
8108
  }
7282
8109
  clearSelection = () => {
7283
8110
  if (this.count() === 0) return;
@@ -7292,6 +8119,43 @@ var MultiSelectionToolbarComponent = class MultiSelectionToolbarComponent {
7292
8119
  });
7293
8120
  openExportDialog = () => void Export.call(this.ids(), { injector: this.injector });
7294
8121
  attachToAssistant = () => this.selection.update({ assistantIdsToAttach: this.ids() });
8122
+ togglePanel() {
8123
+ if (this.panelOpen()) {
8124
+ this.closePanel();
8125
+ return;
8126
+ }
8127
+ this.panelArticles.set([...getState(this.selection).multiSelection]);
8128
+ this.collectionsChanged = false;
8129
+ this.panelOpen.set(true);
8130
+ }
8131
+ closePanel() {
8132
+ const el = this.panel()?.nativeElement;
8133
+ if (el?.matches(":popover-open")) el.hidePopover();
8134
+ else this.panelClosed();
8135
+ }
8136
+ onPanelToggle(event) {
8137
+ if (event.newState === "closed") this.panelClosed();
8138
+ }
8139
+ onFiled(name) {
8140
+ const count = this.panelArticles().length;
8141
+ this.closePanel();
8142
+ this.clearSelection();
8143
+ this.lastFiled.set({
8144
+ count,
8145
+ name
8146
+ });
8147
+ clearTimeout(this.lastFiledTimer);
8148
+ this.lastFiledTimer = setTimeout(() => this.lastFiled.set(null), 5e3);
8149
+ this.addedToCollection.emit(name);
8150
+ }
8151
+ panelClosed() {
8152
+ if (!this.panelOpen()) return;
8153
+ this.panelOpen.set(false);
8154
+ if (this.collectionsChanged) {
8155
+ this.collectionsChanged = false;
8156
+ this.updatedCollections.emit();
8157
+ }
8158
+ }
7295
8159
  static ɵfac = i0.ɵɵngDeclareFactory({
7296
8160
  minVersion: "12.0.0",
7297
8161
  version: "22.2.1",
@@ -7338,26 +8202,38 @@ var MultiSelectionToolbarComponent = class MultiSelectionToolbarComponent {
7338
8202
  },
7339
8203
  outputs: {
7340
8204
  updatedCollections: "updatedCollections",
7341
- cleared: "cleared"
7342
- },
7343
- host: {
7344
- properties: {
7345
- "class.opacity-100": "count() > 0",
7346
- "class.-translate-y-24": "count() > 0"
7347
- },
7348
- classAttribute: "fixed -bottom-20 left-[50%] translate-x-[-50%] z-50 transition-[translate,opacity,discrete] duration-300 opacity-0"
8205
+ cleared: "cleared",
8206
+ addedToCollection: "addedToCollection"
7349
8207
  },
8208
+ host: { properties: { "class": "hostClass()" } },
7350
8209
  providers: [provideTranslocoScope("multi-selection-toolbar", "article", "collections", "export")],
8210
+ viewQueries: [{
8211
+ propertyName: "collectionTrigger",
8212
+ first: true,
8213
+ predicate: ["collectionTrigger"],
8214
+ descendants: true,
8215
+ isSignal: true
8216
+ }, {
8217
+ propertyName: "panel",
8218
+ first: true,
8219
+ predicate: ["panel"],
8220
+ descendants: true,
8221
+ read: ElementRef,
8222
+ isSignal: true
8223
+ }],
7351
8224
  ngImport: i0,
7352
8225
  template: `
7353
8226
  <!-- Région de statut hors de la barre : celle-ci est inerte sans sélection, et une région
7354
- inerte n'est pas annoncée. Le texte n'existe que tant que la sélection est non vide. -->
8227
+ inerte n'est pas annoncée. Le texte n'existe que tant que la sélection est non vide, ou juste
8228
+ après un classement en collection (la sélection vient alors d'être vidée). -->
7355
8229
  <span role="status" class="sr-only">
7356
8230
  @if (count() > 0) {
7357
8231
  {{ 'multiSelectionToolbar.itemsSelected' | transloco: { count: count() } }}
7358
8232
  @if (showHint() && hint()) {
7359
8233
  . {{ hint() }}
7360
8234
  }
8235
+ } @else if (lastFiled(); as filed) {
8236
+ {{ 'collections.addedDocumentsTo' | transloco: filed }}
7361
8237
  }
7362
8238
  </span>
7363
8239
 
@@ -7370,40 +8246,97 @@ var MultiSelectionToolbarComponent = class MultiSelectionToolbarComponent {
7370
8246
  [attr.aria-label]="'multiSelectionToolbar.label' | transloco"
7371
8247
  [attr.inert]="count() === 0 ? '' : null"
7372
8248
  (keydown.escape)="clearSelection()"
7373
- [class]="cn(variants(), class())">
7374
- <div class="ps-0!">
7375
- <button class="cursor-pointer inline-flex items-center gap-1" (click)="clearSelection()">
7376
- {{ 'multiSelectionToolbar.itemsSelected' | transloco: { count: count() } }}
7377
- <x-mark-icon aria-hidden="true" />
7378
- </button>
7379
- </div>
7380
-
7381
- <div>
7382
- <button class="cursor-pointer inline-flex items-center gap-1" (click)="openCollectionDialog()">
7383
- <inbox-icon aria-hidden="true" />
7384
- {{ 'collections.label' | transloco }}
8249
+ [class]="toolbarClass()">
8250
+ @if (variant() === 'floating') {
8251
+ <!-- Le décompte n'est pas répété : la page qui affiche les cases à cocher le montre déjà.
8252
+ Échap vide la sélection depuis n'importe quel bouton. -->
8253
+ <button
8254
+ #collectionTrigger
8255
+ type="button"
8256
+ variant="accent"
8257
+ size="md"
8258
+ aria-haspopup="dialog"
8259
+ [class]="floatingButton"
8260
+ [attr.aria-expanded]="panelOpen()"
8261
+ [attr.aria-controls]="panelOpen() ? panelId : null"
8262
+ (click)="togglePanel()">
8263
+ <bookmark-icon aria-hidden="true" />
8264
+ {{ 'multiSelectionToolbar.addToCollection' | transloco }}
7385
8265
  </button>
7386
- </div>
7387
-
7388
- <div>
7389
- <button class="cursor-pointer inline-flex items-center gap-1" (click)="openExportDialog()">
8266
+ <button type="button" variant="secondary" size="md" [class]="floatingButton + ' bg-(--bg-neutral-white)'" (click)="openExportDialog()">
7390
8267
  <file-output-icon aria-hidden="true" />
7391
8268
  {{ 'export.title' | transloco }}
7392
8269
  </button>
7393
- </div>
7394
-
7395
- @if (showAssistantAction()) {
7396
- <div class="pe-0!">
7397
- <button class="cursor-pointer inline-flex items-center gap-1" (click)="attachToAssistant()">
8270
+ @if (showAssistantAction()) {
8271
+ <button type="button" variant="secondary" size="md" [class]="floatingButton + ' bg-(--bg-neutral-white)'" (click)="attachToAssistant()">
7398
8272
  <sparkles-icon aria-hidden="true" />
7399
8273
  {{ 'article.addToAIOverview' | transloco }}
7400
8274
  </button>
8275
+ }
8276
+ } @else {
8277
+ <div class="ps-0!">
8278
+ <button variant="none" class="cursor-pointer inline-flex items-center gap-1" (click)="clearSelection()">
8279
+ {{ 'multiSelectionToolbar.itemsSelected' | transloco: { count: count() } }}
8280
+ <x-mark-icon aria-hidden="true" />
8281
+ </button>
8282
+ </div>
8283
+
8284
+ <div>
8285
+ <button variant="none" class="cursor-pointer inline-flex items-center gap-1" (click)="openCollectionDialog()">
8286
+ <inbox-icon aria-hidden="true" />
8287
+ {{ 'collections.label' | transloco }}
8288
+ </button>
8289
+ </div>
8290
+
8291
+ <div>
8292
+ <button variant="none" class="cursor-pointer inline-flex items-center gap-1" (click)="openExportDialog()">
8293
+ <file-output-icon aria-hidden="true" />
8294
+ {{ 'export.title' | transloco }}
8295
+ </button>
7401
8296
  </div>
8297
+
8298
+ @if (showAssistantAction()) {
8299
+ <div class="pe-0!">
8300
+ <button variant="none" class="cursor-pointer inline-flex items-center gap-1" (click)="attachToAssistant()">
8301
+ <sparkles-icon aria-hidden="true" />
8302
+ {{ 'article.addToAIOverview' | transloco }}
8303
+ </button>
8304
+ </div>
8305
+ }
7402
8306
  }
7403
8307
  </div>
8308
+
8309
+ <!-- Hors de la barre : un Échap dans le panneau ne doit fermer que lui, pas vider la sélection.
8310
+ popover="auto" : clic à l'extérieur et Échap le ferment, et le navigateur rend alors le focus au
8311
+ bouton qui l'a ouvert. -->
8312
+ @if (panelOpen()) {
8313
+ <AddToCollectionPanel
8314
+ #panel
8315
+ popover="auto"
8316
+ class="fixed m-0 p-0"
8317
+ [id]="panelId"
8318
+ [articles]="panelArticles()"
8319
+ (toggle)="onPanelToggle($event)"
8320
+ (keydown.escape)="$event.stopPropagation()"
8321
+ (changed)="collectionsChanged = true"
8322
+ (done)="onFiled($event)"
8323
+ (closeRequested)="closePanel()" />
8324
+ }
7404
8325
  `,
7405
8326
  isInline: true,
7406
8327
  dependencies: [
8328
+ {
8329
+ kind: "directive",
8330
+ type: ButtonComponent,
8331
+ selector: "button",
8332
+ inputs: [
8333
+ "class",
8334
+ "variant",
8335
+ "scheme",
8336
+ "iconOnly",
8337
+ "size"
8338
+ ]
8339
+ },
7407
8340
  {
7408
8341
  kind: "component",
7409
8342
  type: XMarkIcon,
@@ -7428,6 +8361,23 @@ var MultiSelectionToolbarComponent = class MultiSelectionToolbarComponent {
7428
8361
  selector: "file-output, file-output-icon, FileOutput, FileOutputIcon",
7429
8362
  inputs: ["class"]
7430
8363
  },
8364
+ {
8365
+ kind: "component",
8366
+ type: BookmarkIcon,
8367
+ selector: "bookmark, bookmark-icon, Bookmark, BookmarkIcon",
8368
+ inputs: ["class", "solid"]
8369
+ },
8370
+ {
8371
+ kind: "component",
8372
+ type: AddToCollectionPanelComponent,
8373
+ selector: "AddToCollectionPanel, add-to-collection-panel",
8374
+ inputs: ["articles"],
8375
+ outputs: [
8376
+ "done",
8377
+ "changed",
8378
+ "closeRequested"
8379
+ ]
8380
+ },
7431
8381
  {
7432
8382
  kind: "pipe",
7433
8383
  type: TranslocoPipe,
@@ -7447,21 +8397,27 @@ i0.ɵɵngDeclareClassMetadata({
7447
8397
  selector: "MultiSelectionToolbar, multi-selection-toolbar",
7448
8398
  imports: [
7449
8399
  TranslocoPipe,
8400
+ ButtonComponent,
7450
8401
  XMarkIcon,
7451
8402
  InboxIcon,
7452
8403
  SparklesIcon,
7453
- FileOutputIcon
8404
+ FileOutputIcon,
8405
+ BookmarkIcon,
8406
+ AddToCollectionPanelComponent
7454
8407
  ],
7455
8408
  providers: [provideTranslocoScope("multi-selection-toolbar", "article", "collections", "export")],
7456
8409
  template: `
7457
8410
  <!-- Région de statut hors de la barre : celle-ci est inerte sans sélection, et une région
7458
- inerte n'est pas annoncée. Le texte n'existe que tant que la sélection est non vide. -->
8411
+ inerte n'est pas annoncée. Le texte n'existe que tant que la sélection est non vide, ou juste
8412
+ après un classement en collection (la sélection vient alors d'être vidée). -->
7459
8413
  <span role="status" class="sr-only">
7460
8414
  @if (count() > 0) {
7461
8415
  {{ 'multiSelectionToolbar.itemsSelected' | transloco: { count: count() } }}
7462
8416
  @if (showHint() && hint()) {
7463
8417
  . {{ hint() }}
7464
8418
  }
8419
+ } @else if (lastFiled(); as filed) {
8420
+ {{ 'collections.addedDocumentsTo' | transloco: filed }}
7465
8421
  }
7466
8422
  </span>
7467
8423
 
@@ -7474,43 +8430,84 @@ i0.ɵɵngDeclareClassMetadata({
7474
8430
  [attr.aria-label]="'multiSelectionToolbar.label' | transloco"
7475
8431
  [attr.inert]="count() === 0 ? '' : null"
7476
8432
  (keydown.escape)="clearSelection()"
7477
- [class]="cn(variants(), class())">
7478
- <div class="ps-0!">
7479
- <button class="cursor-pointer inline-flex items-center gap-1" (click)="clearSelection()">
7480
- {{ 'multiSelectionToolbar.itemsSelected' | transloco: { count: count() } }}
7481
- <x-mark-icon aria-hidden="true" />
7482
- </button>
7483
- </div>
7484
-
7485
- <div>
7486
- <button class="cursor-pointer inline-flex items-center gap-1" (click)="openCollectionDialog()">
7487
- <inbox-icon aria-hidden="true" />
7488
- {{ 'collections.label' | transloco }}
8433
+ [class]="toolbarClass()">
8434
+ @if (variant() === 'floating') {
8435
+ <!-- Le décompte n'est pas répété : la page qui affiche les cases à cocher le montre déjà.
8436
+ Échap vide la sélection depuis n'importe quel bouton. -->
8437
+ <button
8438
+ #collectionTrigger
8439
+ type="button"
8440
+ variant="accent"
8441
+ size="md"
8442
+ aria-haspopup="dialog"
8443
+ [class]="floatingButton"
8444
+ [attr.aria-expanded]="panelOpen()"
8445
+ [attr.aria-controls]="panelOpen() ? panelId : null"
8446
+ (click)="togglePanel()">
8447
+ <bookmark-icon aria-hidden="true" />
8448
+ {{ 'multiSelectionToolbar.addToCollection' | transloco }}
7489
8449
  </button>
7490
- </div>
7491
-
7492
- <div>
7493
- <button class="cursor-pointer inline-flex items-center gap-1" (click)="openExportDialog()">
8450
+ <button type="button" variant="secondary" size="md" [class]="floatingButton + ' bg-(--bg-neutral-white)'" (click)="openExportDialog()">
7494
8451
  <file-output-icon aria-hidden="true" />
7495
8452
  {{ 'export.title' | transloco }}
7496
8453
  </button>
7497
- </div>
7498
-
7499
- @if (showAssistantAction()) {
7500
- <div class="pe-0!">
7501
- <button class="cursor-pointer inline-flex items-center gap-1" (click)="attachToAssistant()">
8454
+ @if (showAssistantAction()) {
8455
+ <button type="button" variant="secondary" size="md" [class]="floatingButton + ' bg-(--bg-neutral-white)'" (click)="attachToAssistant()">
7502
8456
  <sparkles-icon aria-hidden="true" />
7503
8457
  {{ 'article.addToAIOverview' | transloco }}
7504
8458
  </button>
8459
+ }
8460
+ } @else {
8461
+ <div class="ps-0!">
8462
+ <button variant="none" class="cursor-pointer inline-flex items-center gap-1" (click)="clearSelection()">
8463
+ {{ 'multiSelectionToolbar.itemsSelected' | transloco: { count: count() } }}
8464
+ <x-mark-icon aria-hidden="true" />
8465
+ </button>
8466
+ </div>
8467
+
8468
+ <div>
8469
+ <button variant="none" class="cursor-pointer inline-flex items-center gap-1" (click)="openCollectionDialog()">
8470
+ <inbox-icon aria-hidden="true" />
8471
+ {{ 'collections.label' | transloco }}
8472
+ </button>
7505
8473
  </div>
8474
+
8475
+ <div>
8476
+ <button variant="none" class="cursor-pointer inline-flex items-center gap-1" (click)="openExportDialog()">
8477
+ <file-output-icon aria-hidden="true" />
8478
+ {{ 'export.title' | transloco }}
8479
+ </button>
8480
+ </div>
8481
+
8482
+ @if (showAssistantAction()) {
8483
+ <div class="pe-0!">
8484
+ <button variant="none" class="cursor-pointer inline-flex items-center gap-1" (click)="attachToAssistant()">
8485
+ <sparkles-icon aria-hidden="true" />
8486
+ {{ 'article.addToAIOverview' | transloco }}
8487
+ </button>
8488
+ </div>
8489
+ }
7506
8490
  }
7507
8491
  </div>
8492
+
8493
+ <!-- Hors de la barre : un Échap dans le panneau ne doit fermer que lui, pas vider la sélection.
8494
+ popover="auto" : clic à l'extérieur et Échap le ferment, et le navigateur rend alors le focus au
8495
+ bouton qui l'a ouvert. -->
8496
+ @if (panelOpen()) {
8497
+ <AddToCollectionPanel
8498
+ #panel
8499
+ popover="auto"
8500
+ class="fixed m-0 p-0"
8501
+ [id]="panelId"
8502
+ [articles]="panelArticles()"
8503
+ (toggle)="onPanelToggle($event)"
8504
+ (keydown.escape)="$event.stopPropagation()"
8505
+ (changed)="collectionsChanged = true"
8506
+ (done)="onFiled($event)"
8507
+ (closeRequested)="closePanel()" />
8508
+ }
7508
8509
  `,
7509
- host: {
7510
- class: "fixed -bottom-20 left-[50%] translate-x-[-50%] z-50 transition-[translate,opacity,discrete] duration-300 opacity-0",
7511
- "[class.opacity-100]": "count() > 0",
7512
- "[class.-translate-y-24]": "count() > 0"
7513
- }
8510
+ host: { "[class]": "hostClass()" }
7514
8511
  }]
7515
8512
  }],
7516
8513
  ctorParameters: () => [],
@@ -7547,6 +8544,10 @@ i0.ɵɵngDeclareClassMetadata({
7547
8544
  type: i0.Output,
7548
8545
  args: ["cleared"]
7549
8546
  }],
8547
+ addedToCollection: [{
8548
+ type: i0.Output,
8549
+ args: ["addedToCollection"]
8550
+ }],
7550
8551
  allowAI: [{
7551
8552
  type: i0.Input,
7552
8553
  args: [{
@@ -7554,6 +8555,17 @@ i0.ɵɵngDeclareClassMetadata({
7554
8555
  alias: "allowAI",
7555
8556
  required: false
7556
8557
  }]
8558
+ }],
8559
+ collectionTrigger: [{
8560
+ type: i0.ViewChild,
8561
+ args: ["collectionTrigger", { isSignal: true }]
8562
+ }],
8563
+ panel: [{
8564
+ type: i0.ViewChild,
8565
+ args: ["panel", {
8566
+ read: ElementRef,
8567
+ isSignal: true
8568
+ }]
7557
8569
  }]
7558
8570
  }
7559
8571
  });
@@ -7962,11 +8974,16 @@ var OverflowManagerDirective = class OverflowManagerDirective {
7962
8974
  });
7963
8975
  target = input(...ngDevMode ? [void 0, { debugName: "target" }] : /* istanbul ignore next */ []);
7964
8976
  margin = input(4, ...ngDevMode ? [{ debugName: "margin" }] : /* istanbul ignore next */ []);
8977
+ fitMargin = input(void 0, ...ngDevMode ? [{ debugName: "fitMargin" }] : /* istanbul ignore next */ []);
7965
8978
  direction = input("horizontal", ...ngDevMode ? [{ debugName: "direction" }] : /* istanbul ignore next */ []);
7966
8979
  reserveStop = input(false, {
7967
8980
  ...ngDevMode ? { debugName: "reserveStop" } : /* istanbul ignore next */ {},
7968
8981
  transform: booleanAttribute
7969
8982
  });
8983
+ animate = input(false, {
8984
+ ...ngDevMode ? { debugName: "animate" } : /* istanbul ignore next */ {},
8985
+ transform: booleanAttribute
8986
+ });
7970
8987
  count = output();
7971
8988
  el = inject(ElementRef).nativeElement;
7972
8989
  destroyRef = inject(DestroyRef);
@@ -7974,6 +8991,7 @@ var OverflowManagerDirective = class OverflowManagerDirective {
7974
8991
  itemsResizeObserver = new ResizeObserver((entries) => {
7975
8992
  if (entries.some((entry) => entry.target.style.display !== "none")) this.countItems();
7976
8993
  });
8994
+ hiddenItems = /* @__PURE__ */ new WeakSet();
7977
8995
  countSub;
7978
8996
  _lastCount;
7979
8997
  constructor() {
@@ -8006,7 +9024,8 @@ var OverflowManagerDirective = class OverflowManagerDirective {
8006
9024
  const containerEnd = (horizontal ? containerRect.right : containerRect.bottom) - this.margin();
8007
9025
  const itemEnd = (rect) => horizontal ? rect.right : rect.bottom;
8008
9026
  let count;
8009
- if (!this.reserveStop() && itemEnd(itemsRects[itemsRects.length - 1]) <= containerEnd) count = itemsRects.length;
9027
+ const fitEnd = (horizontal ? containerRect.right : containerRect.bottom) - (this.fitMargin() ?? this.margin());
9028
+ if (!this.reserveStop() && itemEnd(itemsRects[itemsRects.length - 1]) <= fitEnd) count = itemsRects.length;
8010
9029
  else {
8011
9030
  const limit = containerEnd - (horizontal ? stopRect.width : stopRect.height);
8012
9031
  count = 0;
@@ -8020,7 +9039,19 @@ var OverflowManagerDirective = class OverflowManagerDirective {
8020
9039
  }
8021
9040
  toggleToCount(count) {
8022
9041
  this.items().forEach((item, index) => {
8023
- item.nativeElement.style.display = index < count ? "" : "none";
9042
+ const el = item.nativeElement;
9043
+ const visible = index < count;
9044
+ el.style.display = visible ? "" : "none";
9045
+ if (!visible) this.hiddenItems.add(el);
9046
+ else if (this.hiddenItems.delete(el) && this.animate()) this.fadeIn(el);
9047
+ });
9048
+ }
9049
+ fadeIn(el) {
9050
+ if (typeof el.animate !== "function") return;
9051
+ if (typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches) return;
9052
+ el.animate([{ opacity: 0 }, { opacity: 1 }], {
9053
+ duration: 160,
9054
+ easing: "ease-out"
8024
9055
  });
8025
9056
  }
8026
9057
  static ɵfac = i0.ɵɵngDeclareFactory({
@@ -8052,6 +9083,13 @@ var OverflowManagerDirective = class OverflowManagerDirective {
8052
9083
  isRequired: false,
8053
9084
  transformFunction: null
8054
9085
  },
9086
+ fitMargin: {
9087
+ classPropertyName: "fitMargin",
9088
+ publicName: "fitMargin",
9089
+ isSignal: true,
9090
+ isRequired: false,
9091
+ transformFunction: null
9092
+ },
8055
9093
  direction: {
8056
9094
  classPropertyName: "direction",
8057
9095
  publicName: "direction",
@@ -8065,6 +9103,13 @@ var OverflowManagerDirective = class OverflowManagerDirective {
8065
9103
  isSignal: true,
8066
9104
  isRequired: false,
8067
9105
  transformFunction: null
9106
+ },
9107
+ animate: {
9108
+ classPropertyName: "animate",
9109
+ publicName: "animate",
9110
+ isSignal: true,
9111
+ isRequired: false,
9112
+ transformFunction: null
8068
9113
  }
8069
9114
  },
8070
9115
  outputs: { count: "count" },
@@ -8128,6 +9173,14 @@ i0.ɵɵngDeclareClassMetadata({
8128
9173
  required: false
8129
9174
  }]
8130
9175
  }],
9176
+ fitMargin: [{
9177
+ type: i0.Input,
9178
+ args: [{
9179
+ isSignal: true,
9180
+ alias: "fitMargin",
9181
+ required: false
9182
+ }]
9183
+ }],
8131
9184
  direction: [{
8132
9185
  type: i0.Input,
8133
9186
  args: [{
@@ -8144,6 +9197,14 @@ i0.ɵɵngDeclareClassMetadata({
8144
9197
  required: false
8145
9198
  }]
8146
9199
  }],
9200
+ animate: [{
9201
+ type: i0.Input,
9202
+ args: [{
9203
+ isSignal: true,
9204
+ alias: "animate",
9205
+ required: false
9206
+ }]
9207
+ }],
8147
9208
  count: [{
8148
9209
  type: i0.Output,
8149
9210
  args: ["count"]
@@ -8841,8 +9902,10 @@ var NavbarTabsComponent = class NavbarTabsComponent {
8841
9902
  inputs: [
8842
9903
  "target",
8843
9904
  "margin",
9905
+ "fitMargin",
8844
9906
  "direction",
8845
- "reserveStop"
9907
+ "reserveStop",
9908
+ "animate"
8846
9909
  ],
8847
9910
  outputs: ["count"]
8848
9911
  },
@@ -26208,6 +27271,8 @@ var AggregationDateComponent = class AggregationDateComponent {
26208
27271
  allowCustomRange = inject(FILTER_DATE_ALLOW_CUSTOM_RANGE);
26209
27272
  transloco = inject(TranslocoService);
26210
27273
  injector = inject(Injector);
27274
+ breakpoints = inject(BreakpointObserverService);
27275
+ actionSize = computed(() => this.breakpoints.isTouch() ? "lg" : "md", ...ngDevMode ? [{ debugName: "actionSize" }] : /* istanbul ignore next */ []);
26211
27276
  host = inject(ElementRef);
26212
27277
  selection = signal(false, ...ngDevMode ? [{ debugName: "selection" }] : /* istanbul ignore next */ []);
26213
27278
  validSelection = signal(false, ...ngDevMode ? [{ debugName: "validSelection" }] : /* istanbul ignore next */ []);
@@ -26766,7 +27831,7 @@ var AggregationDateComponent = class AggregationDateComponent {
26766
27831
  host: { properties: { "class": "cn(\"@container flex h-[inherit] max-h-[inherit] flex-col\", class(), isEmpty() && \"hidden\")" } },
26767
27832
  providers: [provideTranslocoScope("filters")],
26768
27833
  ngImport: i0,
26769
- 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=\"lg\"\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=\"lg\"\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 px-1 pt-1 pb-3\"\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 min-h-9 cursor-pointer items-center px-2 py-0.5 text-base text-(--font-neutral-base) select-none [&>radio>label]:min-h-8',\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 items-center [&_.radio-label]:text-base [&_.radio-label]:leading-6 [&_.radio-label]:font-normal\"\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 min-h-9 cursor-pointer items-center gap-(--spacing-xs) px-2 py-0.5 text-base text-(--font-neutral-base) select-none [&>radio>label]:min-h-8 [&>radio>label]:max-w-full [&>radio>label]:min-w-0',\n dateModel().option === 'custom-range' &&\n 'bg-(--bg-primary-lighter)'\n )\n \">\n <radio\n name=\"date-filter\"\n class=\"grow items-center [&_.radio-label]:text-base [&_.radio-label]:leading-6 [&_.radio-label]:font-normal\"\n [checked]=\"dateModel().option === 'custom-range'\"\n (checkedChange)=\"selectOption('custom-range')\">\n <!-- `<radio>` is `display: contents`: its `<label>` is a child of the `<li>` in the box tree but not in the DOM, hence\n the `[&>radio>label]` of the row above. Every box of the chain down to the inputs may shrink (`min-w-0`, no fixed `min-w-[18ch]`): in the 15.5rem popover the\n label, the field and the radio do not fit side by side, and a box that cannot shrink spills out of it and is clipped.\n The fields have a definite width (`w-[18ch]`) that may shrink (`max-w-full min-w-0`). On a touch screen the text is 16px\n instead of 14px, which is what made the row wider than the 15.5rem popover. -->\n <div class=\"flex min-w-0 flex-wrap items-center gap-x-1 gap-y-1\">\n <div class=\"flex min-w-0 items-center gap-1\">\n <label\n [attr.for]=\"startId\"\n class=\"radio-label min-w-8 shrink-0 truncate\"\n >{{ \"filters.from\" | transloco }}</label\n >\n <input-group\n class=\"h-8 w-[18ch] max-w-full min-w-0 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 min-w-0 items-center gap-1\">\n <label\n [attr.for]=\"endId\"\n class=\"radio-label min-w-8 shrink-0 truncate text-right\"\n >{{ \"filters.to\" | transloco }}</label\n >\n <input-group\n class=\"h-8 w-[18ch] max-w-full min-w-0 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 p-0.5 details-content:flex details-content:min-h-0 details-content:flex-col\">\n <!-- `p-0.5` on the details: the one of AggregationPanel (the list and the tree), so the header and its chevron sit on the same\n lines as theirs in the aside filters and the filters panel — without it the chevron was 2px further right.\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=\"md\"\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",
27834
+ 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]=\"actionSize()\"\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]=\"actionSize()\"\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 px-1 pt-1 pb-3\"\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 min-h-9 cursor-pointer items-center px-2 py-0.5 text-base text-(--font-neutral-base) select-none [&>radio>label]:min-h-8',\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 items-center [&_.radio-label]:text-base [&_.radio-label]:leading-6 [&_.radio-label]:font-normal\"\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 min-h-9 cursor-pointer items-center gap-(--spacing-xs) px-2 py-0.5 text-base text-(--font-neutral-base) select-none [&>radio>label]:min-h-8 [&>radio>label]:max-w-full [&>radio>label]:min-w-0',\n dateModel().option === 'custom-range' &&\n 'bg-(--bg-primary-lighter)'\n )\n \">\n <radio\n name=\"date-filter\"\n class=\"grow items-center [&_.radio-label]:text-base [&_.radio-label]:leading-6 [&_.radio-label]:font-normal\"\n [checked]=\"dateModel().option === 'custom-range'\"\n (checkedChange)=\"selectOption('custom-range')\">\n <!-- `<radio>` is `display: contents`: its `<label>` is a child of the `<li>` in the box tree but not in the DOM, hence\n the `[&>radio>label]` of the row above. Every box of the chain down to the inputs may shrink (`min-w-0`, no fixed `min-w-[18ch]`): in the 15.5rem popover the\n label, the field and the radio do not fit side by side, and a box that cannot shrink spills out of it and is clipped.\n The fields have a definite width (`w-[18ch]`) that may shrink (`max-w-full min-w-0`). On a touch screen the text is 16px\n instead of 14px, which is what made the row wider than the 15.5rem popover. -->\n <div class=\"flex min-w-0 flex-wrap items-center gap-x-1 gap-y-1\">\n <div class=\"flex min-w-0 items-center gap-1\">\n <label\n [attr.for]=\"startId\"\n class=\"radio-label min-w-8 shrink-0 truncate\"\n >{{ \"filters.from\" | transloco }}</label\n >\n <input-group\n class=\"h-8 w-[18ch] max-w-full min-w-0 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 min-w-0 items-center gap-1\">\n <label\n [attr.for]=\"endId\"\n class=\"radio-label min-w-8 shrink-0 truncate text-right\"\n >{{ \"filters.to\" | transloco }}</label\n >\n <input-group\n class=\"h-8 w-[18ch] max-w-full min-w-0 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 p-0.5 details-content:flex details-content:min-h-0 details-content:flex-col\">\n <!-- `p-0.5` on the details: the one of AggregationPanel (the list and the tree), so the header and its chevron sit on the same\n lines as theirs in the aside filters and the filters panel — without it the chevron was 2px further right.\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=\"md\"\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",
26770
27835
  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"],
26771
27836
  dependencies: [
26772
27837
  {
@@ -26898,7 +27963,7 @@ i0.ɵɵngDeclareClassMetadata({
26898
27963
  TooltipDirective
26899
27964
  ],
26900
27965
  host: { "[class]": "cn(\"@container flex h-[inherit] max-h-[inherit] flex-col\", class(), isEmpty() && \"hidden\")" },
26901
- 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=\"lg\"\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=\"lg\"\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 px-1 pt-1 pb-3\"\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 min-h-9 cursor-pointer items-center px-2 py-0.5 text-base text-(--font-neutral-base) select-none [&>radio>label]:min-h-8',\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 items-center [&_.radio-label]:text-base [&_.radio-label]:leading-6 [&_.radio-label]:font-normal\"\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 min-h-9 cursor-pointer items-center gap-(--spacing-xs) px-2 py-0.5 text-base text-(--font-neutral-base) select-none [&>radio>label]:min-h-8 [&>radio>label]:max-w-full [&>radio>label]:min-w-0',\n dateModel().option === 'custom-range' &&\n 'bg-(--bg-primary-lighter)'\n )\n \">\n <radio\n name=\"date-filter\"\n class=\"grow items-center [&_.radio-label]:text-base [&_.radio-label]:leading-6 [&_.radio-label]:font-normal\"\n [checked]=\"dateModel().option === 'custom-range'\"\n (checkedChange)=\"selectOption('custom-range')\">\n <!-- `<radio>` is `display: contents`: its `<label>` is a child of the `<li>` in the box tree but not in the DOM, hence\n the `[&>radio>label]` of the row above. Every box of the chain down to the inputs may shrink (`min-w-0`, no fixed `min-w-[18ch]`): in the 15.5rem popover the\n label, the field and the radio do not fit side by side, and a box that cannot shrink spills out of it and is clipped.\n The fields have a definite width (`w-[18ch]`) that may shrink (`max-w-full min-w-0`). On a touch screen the text is 16px\n instead of 14px, which is what made the row wider than the 15.5rem popover. -->\n <div class=\"flex min-w-0 flex-wrap items-center gap-x-1 gap-y-1\">\n <div class=\"flex min-w-0 items-center gap-1\">\n <label\n [attr.for]=\"startId\"\n class=\"radio-label min-w-8 shrink-0 truncate\"\n >{{ \"filters.from\" | transloco }}</label\n >\n <input-group\n class=\"h-8 w-[18ch] max-w-full min-w-0 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 min-w-0 items-center gap-1\">\n <label\n [attr.for]=\"endId\"\n class=\"radio-label min-w-8 shrink-0 truncate text-right\"\n >{{ \"filters.to\" | transloco }}</label\n >\n <input-group\n class=\"h-8 w-[18ch] max-w-full min-w-0 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 p-0.5 details-content:flex details-content:min-h-0 details-content:flex-col\">\n <!-- `p-0.5` on the details: the one of AggregationPanel (the list and the tree), so the header and its chevron sit on the same\n lines as theirs in the aside filters and the filters panel — without it the chevron was 2px further right.\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=\"md\"\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",
27966
+ 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]=\"actionSize()\"\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]=\"actionSize()\"\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 px-1 pt-1 pb-3\"\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 min-h-9 cursor-pointer items-center px-2 py-0.5 text-base text-(--font-neutral-base) select-none [&>radio>label]:min-h-8',\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 items-center [&_.radio-label]:text-base [&_.radio-label]:leading-6 [&_.radio-label]:font-normal\"\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 min-h-9 cursor-pointer items-center gap-(--spacing-xs) px-2 py-0.5 text-base text-(--font-neutral-base) select-none [&>radio>label]:min-h-8 [&>radio>label]:max-w-full [&>radio>label]:min-w-0',\n dateModel().option === 'custom-range' &&\n 'bg-(--bg-primary-lighter)'\n )\n \">\n <radio\n name=\"date-filter\"\n class=\"grow items-center [&_.radio-label]:text-base [&_.radio-label]:leading-6 [&_.radio-label]:font-normal\"\n [checked]=\"dateModel().option === 'custom-range'\"\n (checkedChange)=\"selectOption('custom-range')\">\n <!-- `<radio>` is `display: contents`: its `<label>` is a child of the `<li>` in the box tree but not in the DOM, hence\n the `[&>radio>label]` of the row above. Every box of the chain down to the inputs may shrink (`min-w-0`, no fixed `min-w-[18ch]`): in the 15.5rem popover the\n label, the field and the radio do not fit side by side, and a box that cannot shrink spills out of it and is clipped.\n The fields have a definite width (`w-[18ch]`) that may shrink (`max-w-full min-w-0`). On a touch screen the text is 16px\n instead of 14px, which is what made the row wider than the 15.5rem popover. -->\n <div class=\"flex min-w-0 flex-wrap items-center gap-x-1 gap-y-1\">\n <div class=\"flex min-w-0 items-center gap-1\">\n <label\n [attr.for]=\"startId\"\n class=\"radio-label min-w-8 shrink-0 truncate\"\n >{{ \"filters.from\" | transloco }}</label\n >\n <input-group\n class=\"h-8 w-[18ch] max-w-full min-w-0 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 min-w-0 items-center gap-1\">\n <label\n [attr.for]=\"endId\"\n class=\"radio-label min-w-8 shrink-0 truncate text-right\"\n >{{ \"filters.to\" | transloco }}</label\n >\n <input-group\n class=\"h-8 w-[18ch] max-w-full min-w-0 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 p-0.5 details-content:flex details-content:min-h-0 details-content:flex-col\">\n <!-- `p-0.5` on the details: the one of AggregationPanel (the list and the tree), so the header and its chevron sit on the same\n lines as theirs in the aside filters and the filters panel — without it the chevron was 2px further right.\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=\"md\"\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",
26902
27967
  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"]
26903
27968
  }]
26904
27969
  }],
@@ -27230,6 +28295,8 @@ var AggregationPanelComponent = class AggregationPanelComponent {
27230
28295
  searchBox = viewChild(SearchInputComponent, ...ngDevMode ? [{ debugName: "searchBox" }] : /* istanbul ignore next */ []);
27231
28296
  searchToggle = viewChild("searchToggle", ...ngDevMode ? [{ debugName: "searchToggle" }] : /* istanbul ignore next */ []);
27232
28297
  injector = inject(Injector);
28298
+ breakpoints = inject(BreakpointObserverService);
28299
+ actionSize = computed(() => this.breakpoints.isTouch() ? "lg" : "md", ...ngDevMode ? [{ debugName: "actionSize" }] : /* istanbul ignore next */ []);
27233
28300
  isInPopover = !!inject(PopoverDirective, { optional: true });
27234
28301
  static nextId = 0;
27235
28302
  searchId = `aggregation-search-${AggregationPanelComponent.nextId++}`;
@@ -27434,7 +28501,7 @@ var AggregationPanelComponent = class AggregationPanelComponent {
27434
28501
  isSignal: true
27435
28502
  }],
27436
28503
  ngImport: i0,
27437
- 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=\"md\"\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=\"md\"\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 <!-- `px-0.5`: the box clips what is outside it, and the focus ring of the field is drawn outside the field (1px past its\n border), so it needs a little room on each side. No negative margin to take it back: it pushed that room OUTSIDE\n the box, into the clipping of the popover or the drawer around, and the ring was cut on both sides. -->\n <div class=\"min-h-0 overflow-hidden px-0.5\">\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: clear and apply, and nothing else, so\n both keep their full text in the little room there is. Clear is a button with a text, not an icon, and stays in\n place (disabled) when there is nothing to clear. Apply only shows once something is pending, as it always did, and\n sits at the same level. -->\n <div\n data-bar=\"actions\"\n class=\"mb-1 flex shrink-0 items-center justify-end gap-1\">\n @if (headless() && canSearch()) {\n <ng-container [ngTemplateOutlet]=\"searchToggleSlot\" />\n }\n <button\n type=\"button\"\n variant=\"tertiary\"\n size=\"lg\"\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=\"lg\"\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 @if (itemsLength() > 0) {\n <!-- Select all is the first row of the list, written like one: the box lines up with the boxes of the items it drives and\n carries its own text, which a bare box next to two buttons never did. The text lines up with the items' too: same gap\n after the box (`gap-2`) and same size (`text-sm`, where the checkbox's own label would be 12px). The whole row is the label, so it is as easy\n to hit as an item. The count of checked items is part of that same label (\"Select all (3)\"), so the two can never\n overlap: when the room is short the label is cut with an ellipsis as a whole. It stays out of the scroller of the list, so it does not scroll away with the items. -->\n <div\n data-bar=\"select-all\"\n class=\"mb-1 flex shrink-0 items-center pr-3 pl-1.5 text-sm\">\n <checkbox\n class=\"min-w-0 flex-1 items-center gap-2 py-1.5 [&_.checkbox-label]:truncate [&_.checkbox-label]:text-base [&_.checkbox-label]:leading-6 [&_.checkbox-text-block]:min-w-0\"\n [checked]=\"isAllSelected()\"\n [indeterminate]=\"selectedCount() > 0 && !isAllSelected()\"\n (checkedChange)=\"$event ? allSelected.emit() : allUnselected.emit()\">\n {{\n (isAllSelected() ? \"filters.unselectAll\" : \"filters.selectAll\")\n | transloco\n }}\n @if (selectedCount() > 0) {\n <span class=\"text-(--font-primary-muted)\"\n >({{ selectedCount() }})</span\n >\n }\n </checkbox>\n <span class=\"sr-only\" aria-live=\"polite\">{{\n \"filters.selectedCount\" | transloco: { count: selectedCount() }\n }}</span>\n </div>\n }\n\n <ng-content />\n\n @if (hasMore() && searchedItemsLength() === 0) {\n <!-- The placeholder rows of a loading page are not here: they are rows of the scrolling area, in the list and the tree\n (AggregationLoadMoreSkeleton). Here, between the list and the button, they would shorten the list. -->\n <!-- `variant=\"none\"`: galactik's ButtonComponent matches every <button> and would otherwise dress this one as a primary\n button (filled, rounded, 36px high). `none` leaves the classes below as the only style. -->\n <!-- `aria-disabled`, not `disabled`: the focus stays on the button while the page loads. The accessible name stays\n \"load more\" and `aria-busy` tells that it is working, so no extra text has to be translated by the application. -->\n <button\n type=\"button\"\n data-load-more=\"button\"\n variant=\"none\"\n class=\"mt-1 flex w-full shrink-0 cursor-pointer items-center justify-center rounded-(--radius-xs) px-2 py-1.5 text-(--font-primary-muted) underline underline-offset-4 hover:bg-(--bg-primary-lighter) hover:text-(--font-primary-base) focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-(--stroke-focus) focus-visible:outline-solid aria-busy:cursor-progress aria-busy:no-underline aria-busy:hover:bg-transparent\"\n [attr.aria-label]=\"'loadMore' | transloco\"\n [attr.aria-busy]=\"loading() ? 'true' : null\"\n [attr.aria-disabled]=\"loading() ? 'true' : null\"\n (click)=\"requestMore()\">\n <!-- Both states share one grid cell: the button keeps its width and height when the text gives way to the dots. -->\n <span class=\"inline-grid\">\n <span class=\"col-start-1 row-start-1\" [class.invisible]=\"loading()\">\n {{ \"loadMore\" | transloco }}\n </span>\n @if (loading()) {\n <loading-dots size=\"sm\" class=\"col-start-1 row-start-1\" />\n }\n </span>\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",
28504
+ 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=\"md\"\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=\"md\"\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 <!-- `px-0.5`: the box clips what is outside it, and the focus ring of the field is drawn outside the field (1px past its\n border), so it needs a little room on each side. No negative margin to take it back: it pushed that room OUTSIDE\n the box, into the clipping of the popover or the drawer around, and the ring was cut on both sides. -->\n <div class=\"min-h-0 overflow-hidden px-0.5\">\n <!-- `pb-1.5`: the focus ring of the Clear all / Apply buttons just below sits 4px outside them (2px ring, 2px offset): the\n 6px leave 2px between that ring and the field. -->\n <div [class]=\"headless() ? 'pt-0.5 pb-1.5' : 'pt-2 pb-1.5'\">\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: clear and apply, and nothing else, so\n both keep their full text in the little room there is. Clear is a button with a text, not an icon, and stays in\n place (disabled) when there is nothing to clear. Apply only shows once something is pending, as it always did, and\n sits at the same level. -->\n <div\n data-bar=\"actions\"\n class=\"mb-1 flex shrink-0 items-center justify-end gap-1\">\n @if (headless() && canSearch()) {\n <ng-container [ngTemplateOutlet]=\"searchToggleSlot\" />\n }\n <button\n type=\"button\"\n variant=\"tertiary\"\n [size]=\"actionSize()\"\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]=\"actionSize()\"\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 @if (itemsLength() > 0) {\n <!-- Select all is the first row of the list, written like one: the box lines up with the boxes of the items it drives and\n carries its own text, which a bare box next to two buttons never did. The text lines up with the items' too: same gap\n after the box (`gap-2`) and same size (`text-sm`, where the checkbox's own label would be 12px). The whole row is the label, so it is as easy\n to hit as an item. The count of checked items is part of that same label (\"Select all (3)\"), so the two can never\n overlap: when the room is short the label is cut with an ellipsis as a whole. It stays out of the scroller of the list, so it does not scroll away with the items. -->\n <div\n data-bar=\"select-all\"\n class=\"mb-1 flex shrink-0 items-center pr-3 pl-1.5 text-sm\">\n <checkbox\n class=\"min-w-0 flex-1 items-center gap-2 py-1.5 [&_.checkbox-label]:truncate [&_.checkbox-label]:text-base [&_.checkbox-label]:leading-6 [&_.checkbox-text-block]:min-w-0\"\n [checked]=\"isAllSelected()\"\n [indeterminate]=\"selectedCount() > 0 && !isAllSelected()\"\n (checkedChange)=\"$event ? allSelected.emit() : allUnselected.emit()\">\n {{\n (isAllSelected() ? \"filters.unselectAll\" : \"filters.selectAll\")\n | transloco\n }}\n @if (selectedCount() > 0) {\n <span class=\"text-(--font-primary-muted)\"\n >({{ selectedCount() }})</span\n >\n }\n </checkbox>\n <span class=\"sr-only\" aria-live=\"polite\">{{\n \"filters.selectedCount\" | transloco: { count: selectedCount() }\n }}</span>\n </div>\n }\n\n <ng-content />\n\n @if (hasMore() && searchedItemsLength() === 0) {\n <!-- The placeholder rows of a loading page are not here: they are rows of the scrolling area, in the list and the tree\n (AggregationLoadMoreSkeleton). Here, between the list and the button, they would shorten the list. -->\n <!-- `variant=\"none\"`: galactik's ButtonComponent matches every <button> and would otherwise dress this one as a primary\n button (filled, rounded, 36px high). `none` leaves the classes below as the only style. -->\n <!-- `aria-disabled`, not `disabled`: the focus stays on the button while the page loads. The accessible name stays\n \"load more\" and `aria-busy` tells that it is working, so no extra text has to be translated by the application. -->\n <button\n type=\"button\"\n data-load-more=\"button\"\n variant=\"none\"\n class=\"mt-1 flex w-full shrink-0 cursor-pointer items-center justify-center rounded-(--radius-xs) px-2 py-1.5 text-(--font-primary-muted) underline underline-offset-4 hover:bg-(--bg-primary-lighter) hover:text-(--font-primary-base) focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-(--stroke-focus) focus-visible:outline-solid aria-busy:cursor-progress aria-busy:no-underline aria-busy:hover:bg-transparent\"\n [attr.aria-label]=\"'loadMore' | transloco\"\n [attr.aria-busy]=\"loading() ? 'true' : null\"\n [attr.aria-disabled]=\"loading() ? 'true' : null\"\n (click)=\"requestMore()\">\n <!-- Both states share one grid cell: the button keeps its width and height when the text gives way to the dots. -->\n <span class=\"inline-grid\">\n <span class=\"col-start-1 row-start-1\" [class.invisible]=\"loading()\">\n {{ \"loadMore\" | transloco }}\n </span>\n @if (loading()) {\n <loading-dots size=\"sm\" class=\"col-start-1 row-start-1\" />\n }\n </span>\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",
27438
28505
  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"],
27439
28506
  dependencies: [
27440
28507
  {
@@ -27579,7 +28646,7 @@ i0.ɵɵngDeclareClassMetadata({
27579
28646
  ],
27580
28647
  providers: [provideTranslocoScope("filters")],
27581
28648
  host: { class: "flex min-h-0 flex-1 flex-col" },
27582
- 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=\"md\"\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=\"md\"\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 <!-- `px-0.5`: the box clips what is outside it, and the focus ring of the field is drawn outside the field (1px past its\n border), so it needs a little room on each side. No negative margin to take it back: it pushed that room OUTSIDE\n the box, into the clipping of the popover or the drawer around, and the ring was cut on both sides. -->\n <div class=\"min-h-0 overflow-hidden px-0.5\">\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: clear and apply, and nothing else, so\n both keep their full text in the little room there is. Clear is a button with a text, not an icon, and stays in\n place (disabled) when there is nothing to clear. Apply only shows once something is pending, as it always did, and\n sits at the same level. -->\n <div\n data-bar=\"actions\"\n class=\"mb-1 flex shrink-0 items-center justify-end gap-1\">\n @if (headless() && canSearch()) {\n <ng-container [ngTemplateOutlet]=\"searchToggleSlot\" />\n }\n <button\n type=\"button\"\n variant=\"tertiary\"\n size=\"lg\"\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=\"lg\"\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 @if (itemsLength() > 0) {\n <!-- Select all is the first row of the list, written like one: the box lines up with the boxes of the items it drives and\n carries its own text, which a bare box next to two buttons never did. The text lines up with the items' too: same gap\n after the box (`gap-2`) and same size (`text-sm`, where the checkbox's own label would be 12px). The whole row is the label, so it is as easy\n to hit as an item. The count of checked items is part of that same label (\"Select all (3)\"), so the two can never\n overlap: when the room is short the label is cut with an ellipsis as a whole. It stays out of the scroller of the list, so it does not scroll away with the items. -->\n <div\n data-bar=\"select-all\"\n class=\"mb-1 flex shrink-0 items-center pr-3 pl-1.5 text-sm\">\n <checkbox\n class=\"min-w-0 flex-1 items-center gap-2 py-1.5 [&_.checkbox-label]:truncate [&_.checkbox-label]:text-base [&_.checkbox-label]:leading-6 [&_.checkbox-text-block]:min-w-0\"\n [checked]=\"isAllSelected()\"\n [indeterminate]=\"selectedCount() > 0 && !isAllSelected()\"\n (checkedChange)=\"$event ? allSelected.emit() : allUnselected.emit()\">\n {{\n (isAllSelected() ? \"filters.unselectAll\" : \"filters.selectAll\")\n | transloco\n }}\n @if (selectedCount() > 0) {\n <span class=\"text-(--font-primary-muted)\"\n >({{ selectedCount() }})</span\n >\n }\n </checkbox>\n <span class=\"sr-only\" aria-live=\"polite\">{{\n \"filters.selectedCount\" | transloco: { count: selectedCount() }\n }}</span>\n </div>\n }\n\n <ng-content />\n\n @if (hasMore() && searchedItemsLength() === 0) {\n <!-- The placeholder rows of a loading page are not here: they are rows of the scrolling area, in the list and the tree\n (AggregationLoadMoreSkeleton). Here, between the list and the button, they would shorten the list. -->\n <!-- `variant=\"none\"`: galactik's ButtonComponent matches every <button> and would otherwise dress this one as a primary\n button (filled, rounded, 36px high). `none` leaves the classes below as the only style. -->\n <!-- `aria-disabled`, not `disabled`: the focus stays on the button while the page loads. The accessible name stays\n \"load more\" and `aria-busy` tells that it is working, so no extra text has to be translated by the application. -->\n <button\n type=\"button\"\n data-load-more=\"button\"\n variant=\"none\"\n class=\"mt-1 flex w-full shrink-0 cursor-pointer items-center justify-center rounded-(--radius-xs) px-2 py-1.5 text-(--font-primary-muted) underline underline-offset-4 hover:bg-(--bg-primary-lighter) hover:text-(--font-primary-base) focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-(--stroke-focus) focus-visible:outline-solid aria-busy:cursor-progress aria-busy:no-underline aria-busy:hover:bg-transparent\"\n [attr.aria-label]=\"'loadMore' | transloco\"\n [attr.aria-busy]=\"loading() ? 'true' : null\"\n [attr.aria-disabled]=\"loading() ? 'true' : null\"\n (click)=\"requestMore()\">\n <!-- Both states share one grid cell: the button keeps its width and height when the text gives way to the dots. -->\n <span class=\"inline-grid\">\n <span class=\"col-start-1 row-start-1\" [class.invisible]=\"loading()\">\n {{ \"loadMore\" | transloco }}\n </span>\n @if (loading()) {\n <loading-dots size=\"sm\" class=\"col-start-1 row-start-1\" />\n }\n </span>\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",
28649
+ 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=\"md\"\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=\"md\"\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 <!-- `px-0.5`: the box clips what is outside it, and the focus ring of the field is drawn outside the field (1px past its\n border), so it needs a little room on each side. No negative margin to take it back: it pushed that room OUTSIDE\n the box, into the clipping of the popover or the drawer around, and the ring was cut on both sides. -->\n <div class=\"min-h-0 overflow-hidden px-0.5\">\n <!-- `pb-1.5`: the focus ring of the Clear all / Apply buttons just below sits 4px outside them (2px ring, 2px offset): the\n 6px leave 2px between that ring and the field. -->\n <div [class]=\"headless() ? 'pt-0.5 pb-1.5' : 'pt-2 pb-1.5'\">\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: clear and apply, and nothing else, so\n both keep their full text in the little room there is. Clear is a button with a text, not an icon, and stays in\n place (disabled) when there is nothing to clear. Apply only shows once something is pending, as it always did, and\n sits at the same level. -->\n <div\n data-bar=\"actions\"\n class=\"mb-1 flex shrink-0 items-center justify-end gap-1\">\n @if (headless() && canSearch()) {\n <ng-container [ngTemplateOutlet]=\"searchToggleSlot\" />\n }\n <button\n type=\"button\"\n variant=\"tertiary\"\n [size]=\"actionSize()\"\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]=\"actionSize()\"\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 @if (itemsLength() > 0) {\n <!-- Select all is the first row of the list, written like one: the box lines up with the boxes of the items it drives and\n carries its own text, which a bare box next to two buttons never did. The text lines up with the items' too: same gap\n after the box (`gap-2`) and same size (`text-sm`, where the checkbox's own label would be 12px). The whole row is the label, so it is as easy\n to hit as an item. The count of checked items is part of that same label (\"Select all (3)\"), so the two can never\n overlap: when the room is short the label is cut with an ellipsis as a whole. It stays out of the scroller of the list, so it does not scroll away with the items. -->\n <div\n data-bar=\"select-all\"\n class=\"mb-1 flex shrink-0 items-center pr-3 pl-1.5 text-sm\">\n <checkbox\n class=\"min-w-0 flex-1 items-center gap-2 py-1.5 [&_.checkbox-label]:truncate [&_.checkbox-label]:text-base [&_.checkbox-label]:leading-6 [&_.checkbox-text-block]:min-w-0\"\n [checked]=\"isAllSelected()\"\n [indeterminate]=\"selectedCount() > 0 && !isAllSelected()\"\n (checkedChange)=\"$event ? allSelected.emit() : allUnselected.emit()\">\n {{\n (isAllSelected() ? \"filters.unselectAll\" : \"filters.selectAll\")\n | transloco\n }}\n @if (selectedCount() > 0) {\n <span class=\"text-(--font-primary-muted)\"\n >({{ selectedCount() }})</span\n >\n }\n </checkbox>\n <span class=\"sr-only\" aria-live=\"polite\">{{\n \"filters.selectedCount\" | transloco: { count: selectedCount() }\n }}</span>\n </div>\n }\n\n <ng-content />\n\n @if (hasMore() && searchedItemsLength() === 0) {\n <!-- The placeholder rows of a loading page are not here: they are rows of the scrolling area, in the list and the tree\n (AggregationLoadMoreSkeleton). Here, between the list and the button, they would shorten the list. -->\n <!-- `variant=\"none\"`: galactik's ButtonComponent matches every <button> and would otherwise dress this one as a primary\n button (filled, rounded, 36px high). `none` leaves the classes below as the only style. -->\n <!-- `aria-disabled`, not `disabled`: the focus stays on the button while the page loads. The accessible name stays\n \"load more\" and `aria-busy` tells that it is working, so no extra text has to be translated by the application. -->\n <button\n type=\"button\"\n data-load-more=\"button\"\n variant=\"none\"\n class=\"mt-1 flex w-full shrink-0 cursor-pointer items-center justify-center rounded-(--radius-xs) px-2 py-1.5 text-(--font-primary-muted) underline underline-offset-4 hover:bg-(--bg-primary-lighter) hover:text-(--font-primary-base) focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-(--stroke-focus) focus-visible:outline-solid aria-busy:cursor-progress aria-busy:no-underline aria-busy:hover:bg-transparent\"\n [attr.aria-label]=\"'loadMore' | transloco\"\n [attr.aria-busy]=\"loading() ? 'true' : null\"\n [attr.aria-disabled]=\"loading() ? 'true' : null\"\n (click)=\"requestMore()\">\n <!-- Both states share one grid cell: the button keeps its width and height when the text gives way to the dots. -->\n <span class=\"inline-grid\">\n <span class=\"col-start-1 row-start-1\" [class.invisible]=\"loading()\">\n {{ \"loadMore\" | transloco }}\n </span>\n @if (loading()) {\n <loading-dots size=\"sm\" class=\"col-start-1 row-start-1\" />\n }\n </span>\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",
27583
28650
  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"]
27584
28651
  }]
27585
28652
  }],
@@ -30610,6 +31677,7 @@ function chipsForLeaf(leaf, source) {
30610
31677
  }
30611
31678
  var AppliedFiltersComponent = class AppliedFiltersComponent {
30612
31679
  queryParamsStore = inject(QueryParamsStore);
31680
+ appStore = inject(AppStore);
30613
31681
  static nextId = 0;
30614
31682
  listId = `applied-filters-${AppliedFiltersComponent.nextId++}`;
30615
31683
  list = viewChild("list", ...ngDevMode ? [{ debugName: "list" }] : /* istanbul ignore next */ []);
@@ -30630,6 +31698,10 @@ var AppliedFiltersComponent = class AppliedFiltersComponent {
30630
31698
  ...ngDevMode ? { debugName: "showField" } : /* istanbul ignore next */ {},
30631
31699
  transform: booleanAttribute
30632
31700
  });
31701
+ fieldIcon = input(true, {
31702
+ ...ngDevMode ? { debugName: "fieldIcon" } : /* istanbul ignore next */ {},
31703
+ transform: booleanAttribute
31704
+ });
30633
31705
  variant = input(void 0, ...ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []);
30634
31706
  scheme = input("grey", ...ngDevMode ? [{ debugName: "scheme" }] : /* istanbul ignore next */ []);
30635
31707
  size = input("sm", ...ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []);
@@ -30691,7 +31763,15 @@ var AppliedFiltersComponent = class AppliedFiltersComponent {
30691
31763
  items.forEach((item) => item.inert = !this.expanded() && item.offsetTop - origin >= folded);
30692
31764
  }
30693
31765
  chips = computed(() => {
30694
- return (this.filters() ?? this.queryParamsStore.filters()).flatMap((source) => filterLeaves(source).flatMap((leaf) => chipsForLeaf(leaf, source)));
31766
+ return (this.filters() ?? this.queryParamsStore.filters()).flatMap((source) => filterLeaves(source).flatMap((leaf) => {
31767
+ const chips = chipsForLeaf(leaf, source);
31768
+ const column = chips[0]?.field;
31769
+ const icon = column ? this.appStore.getAggregationCustomization?.(column, chips[0].name)?.icon : void 0;
31770
+ return icon ? chips.map((chip) => ({
31771
+ ...chip,
31772
+ icon
31773
+ })) : chips;
31774
+ }));
30695
31775
  }, ...ngDevMode ? [{ debugName: "chips" }] : /* istanbul ignore next */ []);
30696
31776
  remove(chip) {
30697
31777
  const rebuilt = rebuildWithout(chip.source, chip);
@@ -30758,6 +31838,13 @@ var AppliedFiltersComponent = class AppliedFiltersComponent {
30758
31838
  isRequired: false,
30759
31839
  transformFunction: null
30760
31840
  },
31841
+ fieldIcon: {
31842
+ classPropertyName: "fieldIcon",
31843
+ publicName: "fieldIcon",
31844
+ isSignal: true,
31845
+ isRequired: false,
31846
+ transformFunction: null
31847
+ },
30761
31848
  variant: {
30762
31849
  classPropertyName: "variant",
30763
31850
  publicName: "variant",
@@ -30841,7 +31928,15 @@ var AppliedFiltersComponent = class AppliedFiltersComponent {
30841
31928
  [class]="'max-w-full items-center gap-1 ' + chipClass()"
30842
31929
  >
30843
31930
  @if (showField()) {
30844
- <span>{{ (chip.name ?? chip.field) | syslang }}:</span>
31931
+ @if (fieldIcon() && chip.icon) {
31932
+ <!-- The icon stands for the name: the name stays available as a tooltip and to screen readers. -->
31933
+ <span class="inline-flex shrink-0" [title]="(chip.name ?? chip.field) | syslang">
31934
+ <fa-icon [faClass]="chip.icon" />
31935
+ <span class="sr-only">{{ (chip.name ?? chip.field) | syslang }}:</span>
31936
+ </span>
31937
+ } @else {
31938
+ <span>{{ (chip.name ?? chip.field) | syslang }}:</span>
31939
+ }
30845
31940
  }
30846
31941
  <span class="truncate">{{ chip.display | syslang }}</span>
30847
31942
  <button
@@ -30885,6 +31980,12 @@ var AppliedFiltersComponent = class AppliedFiltersComponent {
30885
31980
  `,
30886
31981
  isInline: true,
30887
31982
  dependencies: [
31983
+ {
31984
+ kind: "component",
31985
+ type: FaIconComponent,
31986
+ selector: "fa-icon, FaIcon",
31987
+ inputs: ["faClass", "class"]
31988
+ },
30888
31989
  {
30889
31990
  kind: "directive",
30890
31991
  type: TagComponent,
@@ -30941,6 +32042,7 @@ i0.ɵɵngDeclareClassMetadata({
30941
32042
  imports: [
30942
32043
  TranslocoPipe,
30943
32044
  SyslangPipe,
32045
+ FaIconComponent,
30944
32046
  TagComponent,
30945
32047
  ButtonComponent,
30946
32048
  XMarkIcon
@@ -30970,7 +32072,15 @@ i0.ɵɵngDeclareClassMetadata({
30970
32072
  [class]="'max-w-full items-center gap-1 ' + chipClass()"
30971
32073
  >
30972
32074
  @if (showField()) {
30973
- <span>{{ (chip.name ?? chip.field) | syslang }}:</span>
32075
+ @if (fieldIcon() && chip.icon) {
32076
+ <!-- The icon stands for the name: the name stays available as a tooltip and to screen readers. -->
32077
+ <span class="inline-flex shrink-0" [title]="(chip.name ?? chip.field) | syslang">
32078
+ <fa-icon [faClass]="chip.icon" />
32079
+ <span class="sr-only">{{ (chip.name ?? chip.field) | syslang }}:</span>
32080
+ </span>
32081
+ } @else {
32082
+ <span>{{ (chip.name ?? chip.field) | syslang }}:</span>
32083
+ }
30974
32084
  }
30975
32085
  <span class="truncate">{{ chip.display | syslang }}</span>
30976
32086
  <button
@@ -31045,6 +32155,14 @@ i0.ɵɵngDeclareClassMetadata({
31045
32155
  required: false
31046
32156
  }]
31047
32157
  }],
32158
+ fieldIcon: [{
32159
+ type: i0.Input,
32160
+ args: [{
32161
+ isSignal: true,
32162
+ alias: "fieldIcon",
32163
+ required: false
32164
+ }]
32165
+ }],
31048
32166
  variant: [{
31049
32167
  type: i0.Input,
31050
32168
  args: [{
@@ -33808,10 +34926,10 @@ var FiltersBarComponent = class FiltersBarComponent {
33808
34926
  height announced as "Filter controls". Every button in it is a native tab stop (no roving, no arrow keys). -->
33809
34927
  <!-- Something of the host's own (a quick-filters pill, say) leads the bar. It sits beside the group and
33810
34928
  not inside it, and it stays visible when the group below is not rendered. -->
33811
- <div [class]="cn('flex min-w-0 items-center rounded-[inherit] bg-inherit', pillGap(), align() === 'center' && 'justify-center')">
34929
+ <div #filtersRow [class]="cn('flex min-w-0 items-center rounded-[inherit] bg-inherit', pillGap(), align() === 'center' && 'justify-center')">
33812
34930
  <ng-content select="[filtersBarStart]" />
33813
34931
  @if (hasContent()) {
33814
- <div role="group" [attr.aria-label]="ariaLabel() ?? ('filters.filterControls' | transloco)" overflowManager [direction]="direction()" [reserveStop]="hasCappedFilters()" (count)="adjustFiltersCount($event)" [class]="cn('flex min-w-0 items-center rounded-[inherit] bg-inherit', pillGap(), align() === 'center' ? 'flex-initial' : 'flex-1')">
34932
+ <div role="group" [attr.aria-label]="ariaLabel() ?? ('filters.filterControls' | transloco)" overflowManager [direction]="direction()" [fitMargin]="-0.5" animate [target]="align() === 'center' ? filtersRow : undefined" [reserveStop]="hasCappedFilters()" (count)="adjustFiltersCount($event)" [class]="cn('relative flex min-w-0 items-center rounded-[inherit] bg-inherit', pillGap(), align() === 'center' ? 'flex-initial' : 'flex-1')">
33815
34933
  @if (hasFilters() && showClearButtons()) {
33816
34934
  <button
33817
34935
  [attr.aria-label]="'filters.clearAllFilters' | transloco"
@@ -33862,7 +34980,10 @@ var FiltersBarComponent = class FiltersBarComponent {
33862
34980
  }
33863
34981
  </div>
33864
34982
 
33865
- <!-- Always mounted so the stop position is stable; hidden when all filters fit -->
34983
+ <!-- Always mounted so the overflow manager can read its size; when all filters fit it is invisible AND out of
34984
+ the flow (absolute), otherwise its reserved width pushes the list aside and an align="center" bar
34985
+ ends up off-center. -->
34986
+
33866
34987
  @if (showMoreFiltersButton()) {
33867
34988
  <!-- No aria-label here: the host is a custom element with no role of its own, where ARIA
33868
34989
  forbids the attribute. The name belongs to the <button> inside MoreButton, which
@@ -33870,7 +34991,7 @@ var FiltersBarComponent = class FiltersBarComponent {
33870
34991
  <MoreButton
33871
34992
  overflowStop
33872
34993
  [disabled]="hasMoreFilters().length === 0"
33873
- [class]="cn('shrink-0', hasMoreFilters().length === 0 && 'invisible pointer-events-none')"
34994
+ [class]="cn('shrink-0', hasMoreFilters().length === 0 && 'invisible pointer-events-none absolute top-0 left-0')"
33874
34995
  [count]="visibleFiltersCount()"
33875
34996
  [position]="morePosition()"
33876
34997
  [lockPosition]="lockPosition()"
@@ -33947,8 +35068,10 @@ var FiltersBarComponent = class FiltersBarComponent {
33947
35068
  inputs: [
33948
35069
  "target",
33949
35070
  "margin",
35071
+ "fitMargin",
33950
35072
  "direction",
33951
- "reserveStop"
35073
+ "reserveStop",
35074
+ "animate"
33952
35075
  ],
33953
35076
  outputs: ["count"]
33954
35077
  },
@@ -34008,10 +35131,10 @@ i0.ɵɵngDeclareClassMetadata({
34008
35131
  height announced as "Filter controls". Every button in it is a native tab stop (no roving, no arrow keys). -->
34009
35132
  <!-- Something of the host's own (a quick-filters pill, say) leads the bar. It sits beside the group and
34010
35133
  not inside it, and it stays visible when the group below is not rendered. -->
34011
- <div [class]="cn('flex min-w-0 items-center rounded-[inherit] bg-inherit', pillGap(), align() === 'center' && 'justify-center')">
35134
+ <div #filtersRow [class]="cn('flex min-w-0 items-center rounded-[inherit] bg-inherit', pillGap(), align() === 'center' && 'justify-center')">
34012
35135
  <ng-content select="[filtersBarStart]" />
34013
35136
  @if (hasContent()) {
34014
- <div role="group" [attr.aria-label]="ariaLabel() ?? ('filters.filterControls' | transloco)" overflowManager [direction]="direction()" [reserveStop]="hasCappedFilters()" (count)="adjustFiltersCount($event)" [class]="cn('flex min-w-0 items-center rounded-[inherit] bg-inherit', pillGap(), align() === 'center' ? 'flex-initial' : 'flex-1')">
35137
+ <div role="group" [attr.aria-label]="ariaLabel() ?? ('filters.filterControls' | transloco)" overflowManager [direction]="direction()" [fitMargin]="-0.5" animate [target]="align() === 'center' ? filtersRow : undefined" [reserveStop]="hasCappedFilters()" (count)="adjustFiltersCount($event)" [class]="cn('relative flex min-w-0 items-center rounded-[inherit] bg-inherit', pillGap(), align() === 'center' ? 'flex-initial' : 'flex-1')">
34015
35138
  @if (hasFilters() && showClearButtons()) {
34016
35139
  <button
34017
35140
  [attr.aria-label]="'filters.clearAllFilters' | transloco"
@@ -34062,7 +35185,10 @@ i0.ɵɵngDeclareClassMetadata({
34062
35185
  }
34063
35186
  </div>
34064
35187
 
34065
- <!-- Always mounted so the stop position is stable; hidden when all filters fit -->
35188
+ <!-- Always mounted so the overflow manager can read its size; when all filters fit it is invisible AND out of
35189
+ the flow (absolute), otherwise its reserved width pushes the list aside and an align="center" bar
35190
+ ends up off-center. -->
35191
+
34066
35192
  @if (showMoreFiltersButton()) {
34067
35193
  <!-- No aria-label here: the host is a custom element with no role of its own, where ARIA
34068
35194
  forbids the attribute. The name belongs to the <button> inside MoreButton, which
@@ -34070,7 +35196,7 @@ i0.ɵɵngDeclareClassMetadata({
34070
35196
  <MoreButton
34071
35197
  overflowStop
34072
35198
  [disabled]="hasMoreFilters().length === 0"
34073
- [class]="cn('shrink-0', hasMoreFilters().length === 0 && 'invisible pointer-events-none')"
35199
+ [class]="cn('shrink-0', hasMoreFilters().length === 0 && 'invisible pointer-events-none absolute top-0 left-0')"
34074
35200
  [count]="visibleFiltersCount()"
34075
35201
  [position]="morePosition()"
34076
35202
  [lockPosition]="lockPosition()"
@@ -41746,6 +42872,6 @@ function provideSessionExpiry(options = {}) {
41746
42872
  const queryNameResolver = () => {
41747
42873
  return inject(AppStore).getDefaultQuery()?.name || "";
41748
42874
  };
41749
- 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, EntityNavigationService, 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, LoadingDotsComponent, LocalizedNumberPipe, MarkdownPipe, MarkdownRendererService, MetadataComponent, MissingTermsComponent, MoreButtonComponent, MoreComponent, MultiSelectLabelsComponent, MultiSelectionToolbarComponent, NON_SEARCHABLE_COLUMNS, NON_SEARCHABLE_DEFAULTS, NavbarTabsComponent, NavigationService, NoResultComponent, OVERFLOW_ACTIONS_CONFIG, OpenArticleOnCtrlEnterDirective, OperatorPipe, OverflowActionsComponent, OverflowItemDirective, OverflowManagerDirective, OverflowStopDirective, OverrideUser, OverrideUserDialogComponent, PREVIEW_ASSISTANT_COMPONENT, PREVIEW_CONFIG, PagerComponent, PreviewActionsComponent, PreviewContentComponent, PreviewFindPanelComponent, 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, SearchBoxPopupDirective, 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, pageNumberFromAnchor, 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 };
42875
+ export { AGGREGATIONS_NAMES, AGGREGATIONS_NAMES_PRESET_DEFAULT, APP_FEATURES, ATOMIC_CLIENT, ATOMIC_SCOPED_PROVIDERS, AddToCollection, AddToCollectionPanelComponent, 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, EntityNavigationService, 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, LoadingDotsComponent, LocalizedNumberPipe, MarkdownPipe, MarkdownRendererService, MetadataComponent, MissingTermsComponent, MoreButtonComponent, MoreComponent, MultiSelectLabelsComponent, MultiSelectionToolbarComponent, NON_SEARCHABLE_COLUMNS, NON_SEARCHABLE_DEFAULTS, NavbarTabsComponent, NavigationService, NoResultComponent, OVERFLOW_ACTIONS_CONFIG, OpenArticleOnCtrlEnterDirective, OperatorPipe, OverflowActionsComponent, OverflowItemDirective, OverflowManagerDirective, OverflowStopDirective, OverrideUser, OverrideUserDialogComponent, PREVIEW_ASSISTANT_COMPONENT, PREVIEW_CONFIG, PagerComponent, PreviewActionsComponent, PreviewContentComponent, PreviewFindPanelComponent, 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, SearchBoxPopupDirective, 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, pageNumberFromAnchor, 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 };
41750
42876
 
41751
42877
  //# sourceMappingURL=sinequa-atomic-angular.mjs.map