@sinequa/atomic-angular 2.2.0 → 2.4.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.
@@ -1,8 +1,8 @@
1
1
  import * as i0 from "@angular/core";
2
2
  import { ChangeDetectionStrategy, ChangeDetectorRef, Component, DOCUMENT, DestroyRef, Directive, ElementRef, EnvironmentInjector, HostBinding, HostListener, Inject, Injectable, InjectionToken, Injector, LOCALE_ID, Optional, Pipe, Service, TemplateRef, ViewEncapsulation, afterNextRender, afterRenderEffect, assertInInjectionContext, booleanAttribute, computed, contentChild, contentChildren, effect, inject, input, isDevMode, linkedSignal, makeEnvironmentProviders, model, numberAttribute, output, provideEnvironmentInitializer, resource, runInInjectionContext, signal, untracked, viewChild, viewChildren } from "@angular/core";
3
- import { BehaviorSubject, EMPTY, Subject, Subscription, catchError, debounceTime, filter, firstValueFrom, from, fromEvent, map, of, shareReplay, switchMap, tap, throwError } from "rxjs";
3
+ import { BehaviorSubject, EMPTY, Subject, Subscription, catchError, debounceTime, filter, firstValueFrom, from, fromEvent, map, of, scan, shareReplay, switchMap, tap, throwError } from "rxjs";
4
4
  import { TranslocoPipe, TranslocoService, provideTranslocoScope } from "@jsverse/transloco";
5
- import { ArrowDownZaIcon, ArrowLeftIcon, ArrowRightFromBracketIcon, ArrowRightIcon, ArrowUpAzIcon, ArrowUpRightFromSquareIcon, ArrowsMaximizeIcon, AutocompleteComponent, AvatarComponent, AvatarFallbackComponent, AvatarImageComponent, BadgeComponent, BarsIcon, BellIcon, BookmarkIcon, BreakpointObserverService, ButtonComponent, CalendarDayIcon, CheckIcon, CheckboxComponent, ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon, ChevronsLeftIcon, ChevronsRightIcon, CircleCheckIcon, CommentIcon, CommentsIcon, DIALOG_REF, DesktopIcon, DialogBodyComponent, DialogComponent, DialogContentComponent, DialogDescriptionComponent, DialogFooterComponent, DialogHeaderComponent, DialogTitleComponent, DropdownDirective, EditIcon, EllipsisIcon, EllipsisVerticalIcon, EyeIcon, EyeSlashIcon, FileHTMLIcon, FileIcon, FileOutputIcon, FileSpreadsheetIcon, FileTextIcon, FilterIcon, FilterXIcon, FlagEnglishIcon, FlagFrenchIcon, FlagGermanIcon, FlashlightIcon, FolderIcon, FolderOpenIcon, FrownIcon, GearIcon, GitForkIcon, GlobeIcon, HighlighterIcon, HistoryIcon, HomeIcon, ImageIcon, InboxIcon, InfoCircleIcon, InputComponent, InputControlDirective, InputIconRightDirective, KbdComponent, KbdGroupComponent, KeyIcon, LanguageIcon, LightbulbIcon, LightbulbSlashIcon, LinkComponent, ListComponent, ListFilterIcon, ListItemActionComponent, ListItemAvatarDirective, ListItemComponent, ListItemLeftDirective, ListItemTextComponent, LoadingCircleIcon, MagnifyingGlassIcon, MagnifyingGlassMinusIcon, MagnifyingGlassPlusIcon, MenuComponent, MenuContentComponent, MenuItemComponent, MenuTriggerDirective, MoonIcon, PaletteIcon, PaperclipIcon, PenToSquareIcon, PlusIcon, PopoverDirective, PresentationIcon, QuestionCircleIcon, RadioComponent, RobotIcon, SaveIcon, SearchBarComponent, SearchInputComponent, SelectComponent, SelectTriggerDirective, SendIcon, SheetBodyComponent, SheetComponent, SheetHeaderComponent, SlashIcon, SlidersIcon, SparklesIcon, SpinnerIcon, SquareCheckIcon, SquareIcon, SquareMinusIcon, StarIcon, SunBrightIcon, SwitchComponent, SyncIcon, TabComponent, TabListComponent, TabPanelComponent, TabsComponent, TagComponent, TagIcon, TagsIcon, TextareaComponent, TextareaControlDirective, ThumbsDownIcon, ThumbsUpIcon, ToggleComponent, ToggleGroupComponent, ToggleItemComponent, ToggleLeftIcon, ToggleRightIcon, TooltipDirective, TrashCanIcon, TrashIcon, TreeComponent, TreeItemComponent, TreeItemExpanderComponent, TreeItemGroupComponent, TriangleAlertIcon, UndoIcon, UserIcon, UserSecretIcon, VideoIcon, XMarkIcon, createCallable, injectCallRef } from "@sinequa/galactik";
5
+ import { ArrowDownZaIcon, ArrowLeftIcon, ArrowRightFromBracketIcon, ArrowRightIcon, ArrowUpAzIcon, ArrowUpRightFromSquareIcon, ArrowsMaximizeIcon, AutocompleteComponent, AvatarComponent, AvatarFallbackComponent, AvatarImageComponent, BadgeComponent, BarsIcon, BellIcon, BookmarkIcon, BreakpointObserverService, ButtonComponent, CalendarDayIcon, CheckIcon, CheckboxComponent, ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon, ChevronsLeftIcon, ChevronsRightIcon, CircleCheckIcon, CommentIcon, CommentsIcon, DIALOG_REF, DesktopIcon, DialogBodyComponent, DialogComponent, DialogContentComponent, DialogDescriptionComponent, DialogFooterComponent, DialogHeaderComponent, DialogTitleComponent, DropdownDirective, EditIcon, EllipsisIcon, EllipsisVerticalIcon, EyeIcon, EyeSlashIcon, FileHTMLIcon, FileIcon, FileOutputIcon, FileSpreadsheetIcon, FileTextIcon, FilterIcon, FilterXIcon, FlagEnglishIcon, FlagFrenchIcon, FlagGermanIcon, FlashlightIcon, FolderIcon, FolderOpenIcon, FrownIcon, GearIcon, GitForkIcon, GlobeIcon, HighlighterIcon, HistoryIcon, HomeIcon, ImageIcon, InboxIcon, InfoCircleIcon, InputComponent, InputControlDirective, InputIconRightDirective, KbdComponent, KbdGroupComponent, KeyIcon, KeyboardIcon, LanguageIcon, LightbulbIcon, LightbulbSlashIcon, LinkComponent, ListComponent, ListFilterIcon, ListItemActionComponent, ListItemAvatarDirective, ListItemComponent, ListItemLeftDirective, ListItemTextComponent, LoadingCircleIcon, MagnifyingGlassIcon, MagnifyingGlassMinusIcon, MagnifyingGlassPlusIcon, MenuComponent, MenuContentComponent, MenuItemComponent, MenuTriggerDirective, MoonIcon, PaletteIcon, PaperclipIcon, PenToSquareIcon, PlusIcon, PopoverDirective, PresentationIcon, QuestionCircleIcon, RadioComponent, RobotIcon, SaveIcon, SearchBarComponent, SearchInputComponent, SelectComponent, SelectTriggerDirective, SendIcon, SheetBodyComponent, SheetComponent, SheetHeaderComponent, SlashIcon, SlidersIcon, SparklesIcon, SpinnerIcon, SquareCheckIcon, SquareIcon, SquareMinusIcon, StarIcon, SunBrightIcon, SwitchComponent, SyncIcon, TabComponent, TabListComponent, TabPanelComponent, TabsComponent, TagComponent, TagIcon, TagsIcon, TextareaComponent, TextareaControlDirective, ThumbsDownIcon, ThumbsUpIcon, ToggleComponent, ToggleGroupComponent, ToggleItemComponent, ToggleLeftIcon, ToggleRightIcon, TooltipDirective, TrashCanIcon, TrashIcon, TreeComponent, TreeItemComponent, TreeItemExpanderComponent, TreeItemGroupComponent, TriangleAlertIcon, UndoIcon, UserIcon, UserSecretIcon, VideoIcon, XMarkIcon, createCallable, injectCallRef } from "@sinequa/galactik";
6
6
  import { withDevtools } from "@angular-architects/ngrx-toolkit";
7
7
  import { getState, patchState, signalStore, signalStoreFeature, withComputed, withMethods, withState } from "@ngrx/signals";
8
8
  import { ActivatedRoute, NavigationEnd, Router, RouterLink, RouterModule } from "@angular/router";
@@ -3354,7 +3354,8 @@ const QUERY_PARAMS_DEFAULTS = {
3354
3354
  aggregations: void 0,
3355
3355
  orderBy: "",
3356
3356
  groupBy: "",
3357
- neuralSearch: void 0
3357
+ neuralSearch: void 0,
3358
+ aggregation_overrides: void 0
3358
3359
  };
3359
3360
  const QueryParamsStore = signalStore({ providedIn: "root" }, withDevtools("QueryParams"), withQueryParamsFeatures());
3360
3361
  function withQueryParamsFeatures() {
@@ -3578,7 +3579,7 @@ function withQueryParamsFeatures() {
3578
3579
  };
3579
3580
  },
3580
3581
  getQuery() {
3581
- const { filters: allFilters = [], sort, tab, basket, text: queryText = "", spellingCorrectionMode } = getState(store);
3582
+ const { filters: allFilters = [], sort, tab, basket, text: queryText = "", spellingCorrectionMode, aggregation_overrides } = getState(store);
3582
3583
  let { name } = getState(store);
3583
3584
  if (!name) name = (route.snapshot.firstChild?.children[0]?.data)?.["queryName"] || appStore.getDefaultQuery()?.name || "";
3584
3585
  let text = queryText;
@@ -3599,7 +3600,8 @@ function withQueryParamsFeatures() {
3599
3600
  spellingCorrectionMode,
3600
3601
  tab,
3601
3602
  text,
3602
- basket
3603
+ basket,
3604
+ aggregation_overrides
3603
3605
  }));
3604
3606
  }
3605
3607
  })));
@@ -20658,9 +20660,7 @@ i0.ɵɵngDeclareClassMetadata({
20658
20660
  }] }
20659
20661
  });
20660
20662
  const FeatureFlags = createCallable(FeatureFlagsDialogComponent);
20661
- var KeyboardShortcutsDialogComponent = class KeyboardShortcutsDialogComponent {
20662
- call = injectCallRef();
20663
- dialog = viewChild(DialogComponent, ...ngDevMode ? [{ debugName: "dialog" }] : /* istanbul ignore next */ []);
20663
+ var KeyboardShortcutsListComponent = class KeyboardShortcutsListComponent {
20664
20664
  keyboard = inject(KeyboardShortcutsService);
20665
20665
  groups = computed(() => {
20666
20666
  const listed = this.keyboard.shortcuts().filter((shortcut) => !shortcut.hidden && shortcut.labelKey);
@@ -20675,6 +20675,146 @@ var KeyboardShortcutsDialogComponent = class KeyboardShortcutsDialogComponent {
20675
20675
  items
20676
20676
  }));
20677
20677
  }, ...ngDevMode ? [{ debugName: "groups" }] : /* istanbul ignore next */ []);
20678
+ static ɵfac = i0.ɵɵngDeclareFactory({
20679
+ minVersion: "12.0.0",
20680
+ version: "22.2.1",
20681
+ ngImport: i0,
20682
+ type: KeyboardShortcutsListComponent,
20683
+ deps: [],
20684
+ target: i0.ɵɵFactoryTarget.Component
20685
+ });
20686
+ static ɵcmp = i0.ɵɵngDeclareComponent({
20687
+ minVersion: "17.0.0",
20688
+ version: "22.2.1",
20689
+ type: KeyboardShortcutsListComponent,
20690
+ isStandalone: true,
20691
+ selector: "keyboard-shortcuts-list",
20692
+ providers: [provideTranslocoScope("dialogs")],
20693
+ ngImport: i0,
20694
+ template: `
20695
+ @if (groups().length === 0) {
20696
+ <p>{{ "dialogs.keyboardShortcuts.empty" | transloco }}</p>
20697
+ }
20698
+
20699
+ @for (group of groups(); track group.titleKey ?? "") {
20700
+ @if (group.titleKey) {
20701
+ <h3 class="mt-(--spacing-md) mb-(--spacing-4xs) text-(length:--size-lg) font-semibold first:mt-0">{{ group.titleKey | transloco }}</h3>
20702
+ }
20703
+ <dl>
20704
+ @for (shortcut of group.items; track shortcut) {
20705
+ <div class="flex items-center justify-between gap-(--spacing-md) py-(--spacing-4xs)">
20706
+ <dt>{{ shortcut.labelKey! | transloco }}</dt>
20707
+ <dd class="flex items-center gap-(--spacing-3xs)">
20708
+ <!-- One group per combination: a shortcut may answer to several (Alt+0 and
20709
+ Shift+?). The group carries the whole combination as its name and the keys
20710
+ are hidden, otherwise a screen reader reads out every key and separator as
20711
+ a separate item. -->
20712
+ <!-- comboIndex is captured explicitly: inside the nested loop $index would
20713
+ refer to the keys, not to the combination.
20714
+ (No backticks in this comment: the template is a template literal.) -->
20715
+ @for (aria of shortcut.aria; track aria; let first = $first, comboIndex = $index) {
20716
+ @if (!first) {
20717
+ <span aria-hidden="true">{{ "dialogs.keyboardShortcuts.or" | transloco }}</span>
20718
+ }
20719
+ <KbdGroup [attr.aria-label]="aria">
20720
+ @for (part of shortcut.displayParts[comboIndex]; track $index; let firstPart = $first) {
20721
+ @if (!firstPart) {
20722
+ <span aria-hidden="true">+</span>
20723
+ }
20724
+ <kbd aria-hidden="true">{{ part }}</kbd>
20725
+ }
20726
+ </KbdGroup>
20727
+ }
20728
+ </dd>
20729
+ </div>
20730
+ }
20731
+ </dl>
20732
+ }
20733
+ `,
20734
+ isInline: true,
20735
+ dependencies: [
20736
+ {
20737
+ kind: "directive",
20738
+ type: KbdComponent,
20739
+ selector: "kbd, Kbd",
20740
+ inputs: ["class", "size"]
20741
+ },
20742
+ {
20743
+ kind: "directive",
20744
+ type: KbdGroupComponent,
20745
+ selector: "kbd-group, kbdgroup, KbdGroup"
20746
+ },
20747
+ {
20748
+ kind: "pipe",
20749
+ type: TranslocoPipe,
20750
+ name: "transloco"
20751
+ }
20752
+ ],
20753
+ changeDetection: i0.ChangeDetectionStrategy.OnPush
20754
+ });
20755
+ };
20756
+ i0.ɵɵngDeclareClassMetadata({
20757
+ minVersion: "12.0.0",
20758
+ version: "22.2.1",
20759
+ ngImport: i0,
20760
+ type: KeyboardShortcutsListComponent,
20761
+ decorators: [{
20762
+ type: Component,
20763
+ args: [{
20764
+ selector: "keyboard-shortcuts-list",
20765
+ changeDetection: ChangeDetectionStrategy.OnPush,
20766
+ imports: [
20767
+ TranslocoPipe,
20768
+ KbdComponent,
20769
+ KbdGroupComponent
20770
+ ],
20771
+ providers: [provideTranslocoScope("dialogs")],
20772
+ template: `
20773
+ @if (groups().length === 0) {
20774
+ <p>{{ "dialogs.keyboardShortcuts.empty" | transloco }}</p>
20775
+ }
20776
+
20777
+ @for (group of groups(); track group.titleKey ?? "") {
20778
+ @if (group.titleKey) {
20779
+ <h3 class="mt-(--spacing-md) mb-(--spacing-4xs) text-(length:--size-lg) font-semibold first:mt-0">{{ group.titleKey | transloco }}</h3>
20780
+ }
20781
+ <dl>
20782
+ @for (shortcut of group.items; track shortcut) {
20783
+ <div class="flex items-center justify-between gap-(--spacing-md) py-(--spacing-4xs)">
20784
+ <dt>{{ shortcut.labelKey! | transloco }}</dt>
20785
+ <dd class="flex items-center gap-(--spacing-3xs)">
20786
+ <!-- One group per combination: a shortcut may answer to several (Alt+0 and
20787
+ Shift+?). The group carries the whole combination as its name and the keys
20788
+ are hidden, otherwise a screen reader reads out every key and separator as
20789
+ a separate item. -->
20790
+ <!-- comboIndex is captured explicitly: inside the nested loop $index would
20791
+ refer to the keys, not to the combination.
20792
+ (No backticks in this comment: the template is a template literal.) -->
20793
+ @for (aria of shortcut.aria; track aria; let first = $first, comboIndex = $index) {
20794
+ @if (!first) {
20795
+ <span aria-hidden="true">{{ "dialogs.keyboardShortcuts.or" | transloco }}</span>
20796
+ }
20797
+ <KbdGroup [attr.aria-label]="aria">
20798
+ @for (part of shortcut.displayParts[comboIndex]; track $index; let firstPart = $first) {
20799
+ @if (!firstPart) {
20800
+ <span aria-hidden="true">+</span>
20801
+ }
20802
+ <kbd aria-hidden="true">{{ part }}</kbd>
20803
+ }
20804
+ </KbdGroup>
20805
+ }
20806
+ </dd>
20807
+ </div>
20808
+ }
20809
+ </dl>
20810
+ }
20811
+ `
20812
+ }]
20813
+ }]
20814
+ });
20815
+ var KeyboardShortcutsDialogComponent = class KeyboardShortcutsDialogComponent {
20816
+ call = injectCallRef();
20817
+ dialog = viewChild(DialogComponent, ...ngDevMode ? [{ debugName: "dialog" }] : /* istanbul ignore next */ []);
20678
20818
  constructor() {
20679
20819
  afterNextRender(() => this.dialog()?.showModal());
20680
20820
  }
@@ -20687,7 +20827,7 @@ var KeyboardShortcutsDialogComponent = class KeyboardShortcutsDialogComponent {
20687
20827
  target: i0.ɵɵFactoryTarget.Component
20688
20828
  });
20689
20829
  static ɵcmp = i0.ɵɵngDeclareComponent({
20690
- minVersion: "17.0.0",
20830
+ minVersion: "17.2.0",
20691
20831
  version: "22.2.1",
20692
20832
  type: KeyboardShortcutsDialogComponent,
20693
20833
  isStandalone: true,
@@ -20709,44 +20849,7 @@ var KeyboardShortcutsDialogComponent = class KeyboardShortcutsDialogComponent {
20709
20849
  </DialogHeader>
20710
20850
 
20711
20851
  <DialogBody>
20712
- @if (groups().length === 0) {
20713
- <p>{{ "dialogs.keyboardShortcuts.empty" | transloco }}</p>
20714
- }
20715
-
20716
- @for (group of groups(); track group.titleKey ?? "") {
20717
- @if (group.titleKey) {
20718
- <h3 class="mt-(--spacing-md) mb-(--spacing-4xs) font-semibold first:mt-0">{{ group.titleKey | transloco }}</h3>
20719
- }
20720
- <dl>
20721
- @for (shortcut of group.items; track shortcut) {
20722
- <div class="flex items-center justify-between gap-(--spacing-md) py-(--spacing-4xs)">
20723
- <dt>{{ shortcut.labelKey! | transloco }}</dt>
20724
- <dd class="flex items-center gap-(--spacing-3xs)">
20725
- <!-- One group per combination: a shortcut may answer to several (Alt+0 and
20726
- Shift+?). The group carries the whole combination as its name and the keys
20727
- are hidden, otherwise a screen reader reads out every key and separator as
20728
- a separate item. -->
20729
- <!-- comboIndex is captured explicitly: inside the nested loop $index would
20730
- refer to the keys, not to the combination.
20731
- (No backticks in this comment: the template is a template literal.) -->
20732
- @for (aria of shortcut.aria; track aria; let first = $first, comboIndex = $index) {
20733
- @if (!first) {
20734
- <span aria-hidden="true">{{ "dialogs.keyboardShortcuts.or" | transloco }}</span>
20735
- }
20736
- <KbdGroup [attr.aria-label]="aria">
20737
- @for (part of shortcut.displayParts[comboIndex]; track $index; let firstPart = $first) {
20738
- @if (!firstPart) {
20739
- <span aria-hidden="true">+</span>
20740
- }
20741
- <kbd aria-hidden="true">{{ part }}</kbd>
20742
- }
20743
- </KbdGroup>
20744
- }
20745
- </dd>
20746
- </div>
20747
- }
20748
- </dl>
20749
- }
20852
+ <keyboard-shortcuts-list />
20750
20853
  </DialogBody>
20751
20854
 
20752
20855
  <DialogFooter>
@@ -20812,15 +20915,9 @@ var KeyboardShortcutsDialogComponent = class KeyboardShortcutsDialogComponent {
20812
20915
  inputs: ["class"]
20813
20916
  },
20814
20917
  {
20815
- kind: "directive",
20816
- type: KbdComponent,
20817
- selector: "kbd, Kbd",
20818
- inputs: ["class", "size"]
20819
- },
20820
- {
20821
- kind: "directive",
20822
- type: KbdGroupComponent,
20823
- selector: "kbd-group, kbdgroup, KbdGroup"
20918
+ kind: "component",
20919
+ type: KeyboardShortcutsListComponent,
20920
+ selector: "keyboard-shortcuts-list"
20824
20921
  },
20825
20922
  {
20826
20923
  kind: "pipe",
@@ -20850,8 +20947,7 @@ i0.ɵɵngDeclareClassMetadata({
20850
20947
  DialogTitleComponent,
20851
20948
  DialogBodyComponent,
20852
20949
  DialogFooterComponent,
20853
- KbdComponent,
20854
- KbdGroupComponent
20950
+ KeyboardShortcutsListComponent
20855
20951
  ],
20856
20952
  providers: [provideTranslocoScope("dialogs")],
20857
20953
  template: `
@@ -20862,44 +20958,7 @@ i0.ɵɵngDeclareClassMetadata({
20862
20958
  </DialogHeader>
20863
20959
 
20864
20960
  <DialogBody>
20865
- @if (groups().length === 0) {
20866
- <p>{{ "dialogs.keyboardShortcuts.empty" | transloco }}</p>
20867
- }
20868
-
20869
- @for (group of groups(); track group.titleKey ?? "") {
20870
- @if (group.titleKey) {
20871
- <h3 class="mt-(--spacing-md) mb-(--spacing-4xs) font-semibold first:mt-0">{{ group.titleKey | transloco }}</h3>
20872
- }
20873
- <dl>
20874
- @for (shortcut of group.items; track shortcut) {
20875
- <div class="flex items-center justify-between gap-(--spacing-md) py-(--spacing-4xs)">
20876
- <dt>{{ shortcut.labelKey! | transloco }}</dt>
20877
- <dd class="flex items-center gap-(--spacing-3xs)">
20878
- <!-- One group per combination: a shortcut may answer to several (Alt+0 and
20879
- Shift+?). The group carries the whole combination as its name and the keys
20880
- are hidden, otherwise a screen reader reads out every key and separator as
20881
- a separate item. -->
20882
- <!-- comboIndex is captured explicitly: inside the nested loop $index would
20883
- refer to the keys, not to the combination.
20884
- (No backticks in this comment: the template is a template literal.) -->
20885
- @for (aria of shortcut.aria; track aria; let first = $first, comboIndex = $index) {
20886
- @if (!first) {
20887
- <span aria-hidden="true">{{ "dialogs.keyboardShortcuts.or" | transloco }}</span>
20888
- }
20889
- <KbdGroup [attr.aria-label]="aria">
20890
- @for (part of shortcut.displayParts[comboIndex]; track $index; let firstPart = $first) {
20891
- @if (!firstPart) {
20892
- <span aria-hidden="true">+</span>
20893
- }
20894
- <kbd aria-hidden="true">{{ part }}</kbd>
20895
- }
20896
- </KbdGroup>
20897
- }
20898
- </dd>
20899
- </div>
20900
- }
20901
- </dl>
20902
- }
20961
+ <keyboard-shortcuts-list />
20903
20962
  </DialogBody>
20904
20963
 
20905
20964
  <DialogFooter>
@@ -22792,6 +22851,7 @@ var AggregationDateComponent = class AggregationDateComponent {
22792
22851
  onClear = output();
22793
22852
  selectionChange = output();
22794
22853
  filtersChange = output();
22854
+ aggregationOverridesChange = output();
22795
22855
  isCollapsed = linkedSignal(() => this.collapsed(), ...ngDevMode ? [{ debugName: "isCollapsed" }] : /* istanbul ignore next */ []);
22796
22856
  expanded = computed(() => this.isCollapsed() ? null : "", ...ngDevMode ? [{ debugName: "expanded" }] : /* istanbul ignore next */ []);
22797
22857
  searchText = model("", ...ngDevMode ? [{ debugName: "searchText" }] : /* istanbul ignore next */ []);
@@ -22915,15 +22975,58 @@ var AggregationDateComponent = class AggregationDateComponent {
22915
22975
  kind: "option",
22916
22976
  option: this.optionToItemValue(option) ?? option
22917
22977
  } : this.toRangeSelection(filter));
22978
+ this.aggregationOverridesChange.emit(this.overridesWithMask(this.selectedMask()));
22918
22979
  this.filtersChange.emit(legacyFilterToFilter(filter));
22919
22980
  }
22981
+ selectedMask() {
22982
+ const option = this.dateModel().option;
22983
+ if (!option || option === "custom-range") return void 0;
22984
+ return this.dateOptions().find((o) => o.display === option)?.mask;
22985
+ }
22986
+ currentOverrides() {
22987
+ return this.isControlled() ? this.controlledQuery()?.aggregation_overrides : this.queryParamsStore.aggregation_overrides?.();
22988
+ }
22989
+ overridesWithMask(mask) {
22990
+ const key = this.aggregation()?.name ?? this.name();
22991
+ const overrides = { ...this.currentOverrides() };
22992
+ if (key) {
22993
+ if (mask) overrides[key] = { mask };
22994
+ else delete overrides[key];
22995
+ }
22996
+ return Object.keys(overrides).length > 0 ? overrides : void 0;
22997
+ }
22998
+ storeMask(mask) {
22999
+ const next = this.overridesWithMask(mask);
23000
+ if (JSON.stringify(next) === JSON.stringify(this.currentOverrides())) return;
23001
+ this.queryParamsStore.patch({ aggregation_overrides: next }, void 0, { syncUrl: false });
23002
+ }
23003
+ maskedLabel(option) {
23004
+ if (!option.mask || !option.value || option.display !== option.value) return void 0;
23005
+ const [year, month] = option.value.split("-").map(Number);
23006
+ const format = option.mask === "YYYY" ? { year: "numeric" } : {
23007
+ year: "numeric",
23008
+ month: "long"
23009
+ };
23010
+ try {
23011
+ return new Intl.DateTimeFormat(this.lang(), {
23012
+ ...format,
23013
+ timeZone: "UTC"
23014
+ }).format(new Date(Date.UTC(year, (month ?? 1) - 1, 1)));
23015
+ } catch {
23016
+ return;
23017
+ }
23018
+ }
22920
23019
  select() {
22921
23020
  this.selection.set(true);
22922
23021
  }
22923
23022
  apply() {
22924
23023
  try {
22925
23024
  if (this.isControlled()) this.emitCurrentSelection();
22926
- else this.queryParamsStore.updateFilter(this.getFormValueFilter(), void 0, { syncUrl: this.syncUrl() });
23025
+ else {
23026
+ const filter = this.getFormValueFilter();
23027
+ this.storeMask(this.selectedMask());
23028
+ this.queryParamsStore.updateFilter(filter, void 0, { syncUrl: this.syncUrl() });
23029
+ }
22927
23030
  this.selection.set(false);
22928
23031
  this.onApply.emit();
22929
23032
  } catch (err) {
@@ -22943,8 +23046,12 @@ var AggregationDateComponent = class AggregationDateComponent {
22943
23046
  if (agg && notify) {
22944
23047
  if (this.isControlled()) {
22945
23048
  this.selectionChange.emit({ kind: "range" });
23049
+ this.aggregationOverridesChange.emit(this.overridesWithMask(void 0));
22946
23050
  this.filtersChange.emit(void 0);
22947
- } else this.queryParamsStore.removeFilterByName(agg.name, void 0, { syncUrl: this.syncUrl() });
23051
+ } else {
23052
+ this.storeMask(void 0);
23053
+ this.queryParamsStore.removeFilterByName(agg.name, void 0, { syncUrl: this.syncUrl() });
23054
+ }
22948
23055
  this.onClear.emit();
22949
23056
  }
22950
23057
  }
@@ -23003,8 +23110,8 @@ var AggregationDateComponent = class AggregationDateComponent {
23003
23110
  this.clear(false);
23004
23111
  return;
23005
23112
  }
23006
- const { operator, value } = filter;
23007
- const code = this.dateOptions().find((option) => option.operator === operator && option.value === value)?.display ?? "custom-range";
23113
+ const { operator } = filter;
23114
+ const code = this.findPresetCode(filter) ?? "custom-range";
23008
23115
  let from = null, to = null;
23009
23116
  if (code === "custom-range") switch (operator) {
23010
23117
  case "lte":
@@ -23017,12 +23124,7 @@ var AggregationDateComponent = class AggregationDateComponent {
23017
23124
  from = filter.start;
23018
23125
  to = filter.end;
23019
23126
  break;
23020
- case "and": {
23021
- const filters = filter.filters;
23022
- from = filters?.find((f) => f.operator === "gte")?.value ?? null;
23023
- to = filters?.find((f) => f.operator === "lte")?.value ?? null;
23024
- break;
23025
- }
23127
+ case "and": ({from, to} = this.toBounds(filter.filters));
23026
23128
  }
23027
23129
  const formValue = {
23028
23130
  option: code,
@@ -23033,6 +23135,29 @@ var AggregationDateComponent = class AggregationDateComponent {
23033
23135
  };
23034
23136
  this.setModel(formValue);
23035
23137
  }
23138
+ findPresetCode(filter) {
23139
+ const options = this.dateOptions();
23140
+ const exact = options.find((option) => option.operator === filter.operator && option.value === filter.value);
23141
+ if (exact) return exact.display;
23142
+ if (filter.operator !== "and") return void 0;
23143
+ const { from, to } = this.toBounds(filter.filters);
23144
+ if (!from || !to) return void 0;
23145
+ return options.find((option) => {
23146
+ if (option.operator !== "and") return false;
23147
+ const bounds = this.toBounds(option.filters);
23148
+ return bounds.from === from && bounds.to === to;
23149
+ })?.display;
23150
+ }
23151
+ toBounds(filters) {
23152
+ const bound = (...operators) => {
23153
+ const value = filters?.find((f) => operators.includes(f.operator))?.value;
23154
+ return value === void 0 || value === null ? null : String(value);
23155
+ };
23156
+ return {
23157
+ from: bound("gte"),
23158
+ to: bound("lte", "lt")
23159
+ };
23160
+ }
23036
23161
  updateFormFromSelection(sel) {
23037
23162
  if (sel?.kind === "option") {
23038
23163
  const code = this.itemValueToOption(sel.option);
@@ -23094,11 +23219,11 @@ var AggregationDateComponent = class AggregationDateComponent {
23094
23219
  to: filter.end
23095
23220
  };
23096
23221
  case "and": {
23097
- const filters = filter.filters ?? [];
23222
+ const { from, to } = this.toBounds(filter.filters);
23098
23223
  return {
23099
23224
  kind: "range",
23100
- from: filters.find((f) => f.operator === "gte")?.value,
23101
- to: filters.find((f) => f.operator === "lte")?.value
23225
+ from: from ?? void 0,
23226
+ to: to ?? void 0
23102
23227
  };
23103
23228
  }
23104
23229
  default: return {
@@ -23287,12 +23412,13 @@ var AggregationDateComponent = class AggregationDateComponent {
23287
23412
  onClear: "onClear",
23288
23413
  selectionChange: "selectionChange",
23289
23414
  filtersChange: "filtersChange",
23415
+ aggregationOverridesChange: "aggregationOverridesChange",
23290
23416
  searchText: "searchTextChange"
23291
23417
  },
23292
23418
  host: { properties: { "class": "cn(\"@container block\", isEmpty() && \"hidden\")" } },
23293
23419
  providers: [provideTranslocoScope("filters")],
23294
23420
  ngImport: i0,
23295
- template: "<!-- `<details>`/`<summary>` stay native (open/close, the `name`-based exclusive accordion). A facet is a\n disclosure, not a grid row: see `aggregation-panel.html` for the why (same pattern, duplicated here\n since this facet type doesn't go through `AggregationPanelComponent`). The `<summary>` itself is\n `tabindex=\"-1\"`: the header's buttons (apply, clear, chevron) are the keyboard stops, see the\n chevron below. -->\n<details [attr.open]=\"expanded()\" [attr.name]=\"id()\" class=\"group space-y-2\">\n <!-- scroll-mb-3/scroll-mt-16: see aggregation-panel-row.html for the full rationale\n (bottom margin against the container's edge, much bigger top margin to also clear\n <More>'s sticky facet-name search box when navigating ArrowUp). -->\n <summary\n tabindex=\"-1\"\n [class.cursor-pointer]=\"collapsible()\"\n [class]=\"rowStateClass\"\n class=\"m-0 flex h-8 w-full scroll-mt-16 scroll-mb-3 items-center gap-1 pl-1 font-semibold select-none\"\n (click)=\"onHeaderClick($event)\">\n <ng-content select=\"label\">\n @let icon = aggregation()?.icon;\n @if (icon) {\n <FaIcon [faClass]=\"icon\" class=\"mr-1 shrink-0\" />\n }\n <span class=\"grow\">{{\n aggregation()?.display | syslang | transloco\n }}</span>\n </ng-content>\n\n @if (hasFilters()) {\n @let clearLabel = \"filters.clearFilters\" | transloco;\n <button\n type=\"button\"\n variant=\"tertiary\"\n [iconOnly]=\"true\"\n size=\"sm\"\n class=\"shrink-0\"\n [aria-label]=\"clearLabel\"\n [tooltip]=\"clearLabel\"\n (click)=\"$event.stopPropagation(); clear()\">\n <filter-x-icon class=\"shrink-0\" />\n </button>\n }\n\n @if (\n (!isControlled() || emitOn() === \"apply\") &&\n selection() &&\n validSelection()\n ) {\n @let label = \"filters.apply\" | transloco;\n <button\n type=\"button\"\n variant=\"accent\"\n size=\"sm\"\n class=\"overflow-hidden\"\n [aria-label]=\"label\"\n [tooltip]=\"label\"\n (click)=\"$event.stopPropagation(); apply()\">\n <FilterIcon class=\"shrink-0\" />\n <span class=\"truncate\">{{ label }}</span>\n </button>\n }\n @if (collapsible()) {\n <!-- The `<summary>` is out of the tab order (`tabindex=\"-1\"`), so this button is the keyboard way to\n collapse the facet. Its click bubbles to the `<summary>`, whose handler does the toggle, and it\n exposes `aria-expanded`. -->\n <button\n type=\"button\"\n variant=\"tertiary\"\n [iconOnly]=\"true\"\n size=\"sm\"\n [aria-label]=\"'filters.toggle' | transloco\"\n [attr.aria-expanded]=\"!isCollapsed()\"\n [tooltip]=\"'filters.toggle' | transloco\"\n class=\"shrink-0 cursor-pointer [&_svg]:transition-transform [&_svg]:duration-150 group-open:[&_svg]:rotate-90\">\n <chevron-right class=\"shrink-0\" />\n </button>\n }\n </summary>\n\n <!-- content wrapper -->\n <!-- RGAA 11.5 / WCAG 1.3.1: the radios below all share one `name`, so they are one set\n and have to be exposed as one. `role=\"group\"` here rather than `role=\"radiogroup\"`\n on the list: a radiogroup owns its radios directly, and the <li> in between would be\n invalid owned children — the list would also stop announcing how many options it\n holds. The name is the aggregation's own title, the words the header already shows;\n with no title the group stays unnamed rather than carrying an empty label. -->\n @let groupName = aggregation()?.display;\n <form (keydown.enter)=\"onOptionEnter($event)\">\n <div\n role=\"group\"\n [attr.aria-label]=\"groupName ? (groupName | syslang | transloco) : null\">\n <ul\n class=\"flex max-h-(--scroll-height,20rem) snap-y snap-start scrollbar-thin flex-col gap-0.5 overflow-auto pt-2\"\n role=\"list\">\n @for (option of dateOptions(); track $index) {\n <li\n role=\"listitem\"\n [attr.aria-label]=\"option.display | syslang | transloco\"\n [class]=\"\n cn(\n itemStateClass,\n 'flex cursor-pointer items-center p-2 text-sm text-(--font-neutral-base) select-none',\n dateModel().option === option.display &&\n 'bg-(--bg-primary-lighter)',\n option.hidden && 'hidden',\n option.disabled && 'pointer-events-none opacity-40'\n )\n \"\n [attr.aria-hidden]=\"option.disabled\">\n <radio\n name=\"date-filter\"\n class=\"grow\"\n [checked]=\"dateModel().option === option.display\"\n [disabled]=\"option.disabled\"\n (checkedChange)=\"selectOption(option.display)\">\n {{ option.display | syslang | transloco }}\n </radio>\n </li>\n }\n\n @if (allowCustomRange) {\n <li\n role=\"listitem\"\n aria-label=\"open date range picker\"\n [class]=\"\n cn(\n itemStateClass,\n 'flex cursor-pointer items-center gap-(--spacing-xs) p-2 text-sm text-(--font-neutral-base) select-none',\n dateModel().option === 'custom-range' &&\n 'bg-(--bg-primary-lighter)'\n )\n \">\n <radio\n name=\"date-filter\"\n class=\"grow items-center\"\n [checked]=\"dateModel().option === 'custom-range'\"\n (checkedChange)=\"selectOption('custom-range')\">\n <div class=\"flex flex-wrap items-center gap-x-1 gap-y-1\">\n <div class=\"flex items-center gap-1\">\n <label\n [attr.for]=\"startId\"\n class=\"radio-label min-w-8 truncate\"\n >{{ \"filters.from\" | transloco }}</label\n >\n <input-group\n class=\"h-8 max-w-[18ch] min-w-[18ch] has-[[readonly]]:border-(--stroke-primary-muted)\">\n <input\n input-control\n [id]=\"startId\"\n name=\"start\"\n type=\"text\"\n readonly\n [value]=\"customRangeFrom()\"\n (click)=\"selectAndOpenDialog()\"\n (keydown.enter)=\"onRangeInputEnter($event)\" />\n </input-group>\n </div>\n <div class=\"flex items-center gap-1\">\n <label\n [attr.for]=\"endId\"\n class=\"radio-label min-w-8 truncate text-right\"\n >{{ \"filters.to\" | transloco }}</label\n >\n <input-group\n class=\"h-8 max-w-[18ch] min-w-[18ch] has-[[readonly]]:border-(--stroke-primary-muted)\">\n <input\n input-control\n [id]=\"endId\"\n name=\"end\"\n type=\"text\"\n readonly\n [value]=\"customRangeTo()\"\n (click)=\"selectAndOpenDialog()\"\n (keydown.enter)=\"onRangeInputEnter($event)\" />\n </input-group>\n </div>\n </div>\n </radio>\n </li>\n }\n </ul>\n </div>\n </form>\n</details>\n",
23421
+ template: "<!-- `<details>`/`<summary>` stay native (open/close, the `name`-based exclusive accordion). A facet is a\n disclosure, not a grid row: see `aggregation-panel.html` for the why (same pattern, duplicated here\n since this facet type doesn't go through `AggregationPanelComponent`). The `<summary>` itself is\n `tabindex=\"-1\"`: the header's buttons (apply, clear, chevron) are the keyboard stops, see the\n chevron below. -->\n<details [attr.open]=\"expanded()\" [attr.name]=\"id()\" class=\"group space-y-2\">\n <!-- scroll-mb-3/scroll-mt-16: see aggregation-panel-row.html for the full rationale\n (bottom margin against the container's edge, much bigger top margin to also clear\n <More>'s sticky facet-name search box when navigating ArrowUp). -->\n <summary\n tabindex=\"-1\"\n [class.cursor-pointer]=\"collapsible()\"\n [class]=\"rowStateClass\"\n class=\"m-0 flex h-8 w-full scroll-mt-16 scroll-mb-3 items-center gap-1 pl-1 font-semibold select-none\"\n (click)=\"onHeaderClick($event)\">\n <ng-content select=\"label\">\n @let icon = aggregation()?.icon;\n @if (icon) {\n <FaIcon [faClass]=\"icon\" class=\"mr-1 shrink-0\" />\n }\n <span class=\"grow\">{{\n aggregation()?.display | syslang | transloco\n }}</span>\n </ng-content>\n\n @if (hasFilters()) {\n @let clearLabel = \"filters.clearFilters\" | transloco;\n <button\n type=\"button\"\n variant=\"tertiary\"\n [iconOnly]=\"true\"\n size=\"sm\"\n class=\"shrink-0\"\n [aria-label]=\"clearLabel\"\n [tooltip]=\"clearLabel\"\n (click)=\"$event.stopPropagation(); clear()\">\n <filter-x-icon class=\"shrink-0\" />\n </button>\n }\n\n @if (\n (!isControlled() || emitOn() === \"apply\") &&\n selection() &&\n validSelection()\n ) {\n @let label = \"filters.apply\" | transloco;\n <button\n type=\"button\"\n variant=\"accent\"\n size=\"sm\"\n class=\"overflow-hidden\"\n [aria-label]=\"label\"\n [tooltip]=\"label\"\n (click)=\"$event.stopPropagation(); apply()\">\n <FilterIcon class=\"shrink-0\" />\n <span class=\"truncate\">{{ label }}</span>\n </button>\n }\n @if (collapsible()) {\n <!-- The `<summary>` is out of the tab order (`tabindex=\"-1\"`), so this button is the keyboard way to\n collapse the facet. Its click bubbles to the `<summary>`, whose handler does the toggle, and it\n exposes `aria-expanded`. -->\n <button\n type=\"button\"\n variant=\"tertiary\"\n [iconOnly]=\"true\"\n size=\"sm\"\n [aria-label]=\"'filters.toggle' | transloco\"\n [attr.aria-expanded]=\"!isCollapsed()\"\n [tooltip]=\"'filters.toggle' | transloco\"\n class=\"shrink-0 cursor-pointer [&_svg]:transition-transform [&_svg]:duration-150 group-open:[&_svg]:rotate-90\">\n <chevron-right class=\"shrink-0\" />\n </button>\n }\n </summary>\n\n <!-- content wrapper -->\n <!-- RGAA 11.5 / WCAG 1.3.1: the radios below all share one `name`, so they are one set\n and have to be exposed as one. `role=\"group\"` here rather than `role=\"radiogroup\"`\n on the list: a radiogroup owns its radios directly, and the <li> in between would be\n invalid owned children — the list would also stop announcing how many options it\n holds. The name is the aggregation's own title, the words the header already shows;\n with no title the group stays unnamed rather than carrying an empty label. -->\n @let groupName = aggregation()?.display;\n <form (keydown.enter)=\"onOptionEnter($event)\">\n <div\n role=\"group\"\n [attr.aria-label]=\"groupName ? (groupName | syslang | transloco) : null\">\n <ul\n class=\"flex max-h-(--scroll-height,20rem) snap-y snap-start scrollbar-thin flex-col gap-0.5 overflow-auto pt-2\"\n role=\"list\">\n @for (option of dateOptions(); track option.display) {\n <li\n role=\"listitem\"\n [attr.aria-label]=\"\n maskedLabel(option) ?? (option.display | syslang | transloco)\n \"\n [class]=\"\n cn(\n itemStateClass,\n 'flex cursor-pointer items-center p-2 text-sm text-(--font-neutral-base) select-none',\n dateModel().option === option.display &&\n 'bg-(--bg-primary-lighter)',\n option.hidden && 'hidden',\n option.disabled && 'pointer-events-none opacity-40'\n )\n \"\n [attr.aria-hidden]=\"option.disabled\">\n <radio\n name=\"date-filter\"\n class=\"grow\"\n [checked]=\"dateModel().option === option.display\"\n [disabled]=\"option.disabled\"\n (checkedChange)=\"selectOption(option.display)\">\n {{\n maskedLabel(option) ?? (option.display | syslang | transloco)\n }}\n </radio>\n </li>\n }\n\n @if (allowCustomRange) {\n <li\n role=\"listitem\"\n aria-label=\"open date range picker\"\n [class]=\"\n cn(\n itemStateClass,\n 'flex cursor-pointer items-center gap-(--spacing-xs) p-2 text-sm text-(--font-neutral-base) select-none',\n dateModel().option === 'custom-range' &&\n 'bg-(--bg-primary-lighter)'\n )\n \">\n <radio\n name=\"date-filter\"\n class=\"grow items-center\"\n [checked]=\"dateModel().option === 'custom-range'\"\n (checkedChange)=\"selectOption('custom-range')\">\n <div class=\"flex flex-wrap items-center gap-x-1 gap-y-1\">\n <div class=\"flex items-center gap-1\">\n <label\n [attr.for]=\"startId\"\n class=\"radio-label min-w-8 truncate\"\n >{{ \"filters.from\" | transloco }}</label\n >\n <input-group\n class=\"h-8 max-w-[18ch] min-w-[18ch] has-[[readonly]]:border-(--stroke-primary-muted)\">\n <input\n input-control\n [id]=\"startId\"\n name=\"start\"\n type=\"text\"\n readonly\n [value]=\"customRangeFrom()\"\n (click)=\"selectAndOpenDialog()\"\n (keydown.enter)=\"onRangeInputEnter($event)\" />\n </input-group>\n </div>\n <div class=\"flex items-center gap-1\">\n <label\n [attr.for]=\"endId\"\n class=\"radio-label min-w-8 truncate text-right\"\n >{{ \"filters.to\" | transloco }}</label\n >\n <input-group\n class=\"h-8 max-w-[18ch] min-w-[18ch] has-[[readonly]]:border-(--stroke-primary-muted)\">\n <input\n input-control\n [id]=\"endId\"\n name=\"end\"\n type=\"text\"\n readonly\n [value]=\"customRangeTo()\"\n (click)=\"selectAndOpenDialog()\"\n (keydown.enter)=\"onRangeInputEnter($event)\" />\n </input-group>\n </div>\n </div>\n </radio>\n </li>\n }\n </ul>\n </div>\n </form>\n</details>\n",
23296
23422
  styles: [":host{min-width:200px}ul[role=list]{scrollbar-width:thin}\n"],
23297
23423
  dependencies: [
23298
23424
  {
@@ -23427,7 +23553,7 @@ i0.ɵɵngDeclareClassMetadata({
23427
23553
  TooltipDirective
23428
23554
  ],
23429
23555
  host: { "[class]": "cn(\"@container block\", isEmpty() && \"hidden\")" },
23430
- template: "<!-- `<details>`/`<summary>` stay native (open/close, the `name`-based exclusive accordion). A facet is a\n disclosure, not a grid row: see `aggregation-panel.html` for the why (same pattern, duplicated here\n since this facet type doesn't go through `AggregationPanelComponent`). The `<summary>` itself is\n `tabindex=\"-1\"`: the header's buttons (apply, clear, chevron) are the keyboard stops, see the\n chevron below. -->\n<details [attr.open]=\"expanded()\" [attr.name]=\"id()\" class=\"group space-y-2\">\n <!-- scroll-mb-3/scroll-mt-16: see aggregation-panel-row.html for the full rationale\n (bottom margin against the container's edge, much bigger top margin to also clear\n <More>'s sticky facet-name search box when navigating ArrowUp). -->\n <summary\n tabindex=\"-1\"\n [class.cursor-pointer]=\"collapsible()\"\n [class]=\"rowStateClass\"\n class=\"m-0 flex h-8 w-full scroll-mt-16 scroll-mb-3 items-center gap-1 pl-1 font-semibold select-none\"\n (click)=\"onHeaderClick($event)\">\n <ng-content select=\"label\">\n @let icon = aggregation()?.icon;\n @if (icon) {\n <FaIcon [faClass]=\"icon\" class=\"mr-1 shrink-0\" />\n }\n <span class=\"grow\">{{\n aggregation()?.display | syslang | transloco\n }}</span>\n </ng-content>\n\n @if (hasFilters()) {\n @let clearLabel = \"filters.clearFilters\" | transloco;\n <button\n type=\"button\"\n variant=\"tertiary\"\n [iconOnly]=\"true\"\n size=\"sm\"\n class=\"shrink-0\"\n [aria-label]=\"clearLabel\"\n [tooltip]=\"clearLabel\"\n (click)=\"$event.stopPropagation(); clear()\">\n <filter-x-icon class=\"shrink-0\" />\n </button>\n }\n\n @if (\n (!isControlled() || emitOn() === \"apply\") &&\n selection() &&\n validSelection()\n ) {\n @let label = \"filters.apply\" | transloco;\n <button\n type=\"button\"\n variant=\"accent\"\n size=\"sm\"\n class=\"overflow-hidden\"\n [aria-label]=\"label\"\n [tooltip]=\"label\"\n (click)=\"$event.stopPropagation(); apply()\">\n <FilterIcon class=\"shrink-0\" />\n <span class=\"truncate\">{{ label }}</span>\n </button>\n }\n @if (collapsible()) {\n <!-- The `<summary>` is out of the tab order (`tabindex=\"-1\"`), so this button is the keyboard way to\n collapse the facet. Its click bubbles to the `<summary>`, whose handler does the toggle, and it\n exposes `aria-expanded`. -->\n <button\n type=\"button\"\n variant=\"tertiary\"\n [iconOnly]=\"true\"\n size=\"sm\"\n [aria-label]=\"'filters.toggle' | transloco\"\n [attr.aria-expanded]=\"!isCollapsed()\"\n [tooltip]=\"'filters.toggle' | transloco\"\n class=\"shrink-0 cursor-pointer [&_svg]:transition-transform [&_svg]:duration-150 group-open:[&_svg]:rotate-90\">\n <chevron-right class=\"shrink-0\" />\n </button>\n }\n </summary>\n\n <!-- content wrapper -->\n <!-- RGAA 11.5 / WCAG 1.3.1: the radios below all share one `name`, so they are one set\n and have to be exposed as one. `role=\"group\"` here rather than `role=\"radiogroup\"`\n on the list: a radiogroup owns its radios directly, and the <li> in between would be\n invalid owned children — the list would also stop announcing how many options it\n holds. The name is the aggregation's own title, the words the header already shows;\n with no title the group stays unnamed rather than carrying an empty label. -->\n @let groupName = aggregation()?.display;\n <form (keydown.enter)=\"onOptionEnter($event)\">\n <div\n role=\"group\"\n [attr.aria-label]=\"groupName ? (groupName | syslang | transloco) : null\">\n <ul\n class=\"flex max-h-(--scroll-height,20rem) snap-y snap-start scrollbar-thin flex-col gap-0.5 overflow-auto pt-2\"\n role=\"list\">\n @for (option of dateOptions(); track $index) {\n <li\n role=\"listitem\"\n [attr.aria-label]=\"option.display | syslang | transloco\"\n [class]=\"\n cn(\n itemStateClass,\n 'flex cursor-pointer items-center p-2 text-sm text-(--font-neutral-base) select-none',\n dateModel().option === option.display &&\n 'bg-(--bg-primary-lighter)',\n option.hidden && 'hidden',\n option.disabled && 'pointer-events-none opacity-40'\n )\n \"\n [attr.aria-hidden]=\"option.disabled\">\n <radio\n name=\"date-filter\"\n class=\"grow\"\n [checked]=\"dateModel().option === option.display\"\n [disabled]=\"option.disabled\"\n (checkedChange)=\"selectOption(option.display)\">\n {{ option.display | syslang | transloco }}\n </radio>\n </li>\n }\n\n @if (allowCustomRange) {\n <li\n role=\"listitem\"\n aria-label=\"open date range picker\"\n [class]=\"\n cn(\n itemStateClass,\n 'flex cursor-pointer items-center gap-(--spacing-xs) p-2 text-sm text-(--font-neutral-base) select-none',\n dateModel().option === 'custom-range' &&\n 'bg-(--bg-primary-lighter)'\n )\n \">\n <radio\n name=\"date-filter\"\n class=\"grow items-center\"\n [checked]=\"dateModel().option === 'custom-range'\"\n (checkedChange)=\"selectOption('custom-range')\">\n <div class=\"flex flex-wrap items-center gap-x-1 gap-y-1\">\n <div class=\"flex items-center gap-1\">\n <label\n [attr.for]=\"startId\"\n class=\"radio-label min-w-8 truncate\"\n >{{ \"filters.from\" | transloco }}</label\n >\n <input-group\n class=\"h-8 max-w-[18ch] min-w-[18ch] has-[[readonly]]:border-(--stroke-primary-muted)\">\n <input\n input-control\n [id]=\"startId\"\n name=\"start\"\n type=\"text\"\n readonly\n [value]=\"customRangeFrom()\"\n (click)=\"selectAndOpenDialog()\"\n (keydown.enter)=\"onRangeInputEnter($event)\" />\n </input-group>\n </div>\n <div class=\"flex items-center gap-1\">\n <label\n [attr.for]=\"endId\"\n class=\"radio-label min-w-8 truncate text-right\"\n >{{ \"filters.to\" | transloco }}</label\n >\n <input-group\n class=\"h-8 max-w-[18ch] min-w-[18ch] has-[[readonly]]:border-(--stroke-primary-muted)\">\n <input\n input-control\n [id]=\"endId\"\n name=\"end\"\n type=\"text\"\n readonly\n [value]=\"customRangeTo()\"\n (click)=\"selectAndOpenDialog()\"\n (keydown.enter)=\"onRangeInputEnter($event)\" />\n </input-group>\n </div>\n </div>\n </radio>\n </li>\n }\n </ul>\n </div>\n </form>\n</details>\n",
23556
+ template: "<!-- `<details>`/`<summary>` stay native (open/close, the `name`-based exclusive accordion). A facet is a\n disclosure, not a grid row: see `aggregation-panel.html` for the why (same pattern, duplicated here\n since this facet type doesn't go through `AggregationPanelComponent`). The `<summary>` itself is\n `tabindex=\"-1\"`: the header's buttons (apply, clear, chevron) are the keyboard stops, see the\n chevron below. -->\n<details [attr.open]=\"expanded()\" [attr.name]=\"id()\" class=\"group space-y-2\">\n <!-- scroll-mb-3/scroll-mt-16: see aggregation-panel-row.html for the full rationale\n (bottom margin against the container's edge, much bigger top margin to also clear\n <More>'s sticky facet-name search box when navigating ArrowUp). -->\n <summary\n tabindex=\"-1\"\n [class.cursor-pointer]=\"collapsible()\"\n [class]=\"rowStateClass\"\n class=\"m-0 flex h-8 w-full scroll-mt-16 scroll-mb-3 items-center gap-1 pl-1 font-semibold select-none\"\n (click)=\"onHeaderClick($event)\">\n <ng-content select=\"label\">\n @let icon = aggregation()?.icon;\n @if (icon) {\n <FaIcon [faClass]=\"icon\" class=\"mr-1 shrink-0\" />\n }\n <span class=\"grow\">{{\n aggregation()?.display | syslang | transloco\n }}</span>\n </ng-content>\n\n @if (hasFilters()) {\n @let clearLabel = \"filters.clearFilters\" | transloco;\n <button\n type=\"button\"\n variant=\"tertiary\"\n [iconOnly]=\"true\"\n size=\"sm\"\n class=\"shrink-0\"\n [aria-label]=\"clearLabel\"\n [tooltip]=\"clearLabel\"\n (click)=\"$event.stopPropagation(); clear()\">\n <filter-x-icon class=\"shrink-0\" />\n </button>\n }\n\n @if (\n (!isControlled() || emitOn() === \"apply\") &&\n selection() &&\n validSelection()\n ) {\n @let label = \"filters.apply\" | transloco;\n <button\n type=\"button\"\n variant=\"accent\"\n size=\"sm\"\n class=\"overflow-hidden\"\n [aria-label]=\"label\"\n [tooltip]=\"label\"\n (click)=\"$event.stopPropagation(); apply()\">\n <FilterIcon class=\"shrink-0\" />\n <span class=\"truncate\">{{ label }}</span>\n </button>\n }\n @if (collapsible()) {\n <!-- The `<summary>` is out of the tab order (`tabindex=\"-1\"`), so this button is the keyboard way to\n collapse the facet. Its click bubbles to the `<summary>`, whose handler does the toggle, and it\n exposes `aria-expanded`. -->\n <button\n type=\"button\"\n variant=\"tertiary\"\n [iconOnly]=\"true\"\n size=\"sm\"\n [aria-label]=\"'filters.toggle' | transloco\"\n [attr.aria-expanded]=\"!isCollapsed()\"\n [tooltip]=\"'filters.toggle' | transloco\"\n class=\"shrink-0 cursor-pointer [&_svg]:transition-transform [&_svg]:duration-150 group-open:[&_svg]:rotate-90\">\n <chevron-right class=\"shrink-0\" />\n </button>\n }\n </summary>\n\n <!-- content wrapper -->\n <!-- RGAA 11.5 / WCAG 1.3.1: the radios below all share one `name`, so they are one set\n and have to be exposed as one. `role=\"group\"` here rather than `role=\"radiogroup\"`\n on the list: a radiogroup owns its radios directly, and the <li> in between would be\n invalid owned children — the list would also stop announcing how many options it\n holds. The name is the aggregation's own title, the words the header already shows;\n with no title the group stays unnamed rather than carrying an empty label. -->\n @let groupName = aggregation()?.display;\n <form (keydown.enter)=\"onOptionEnter($event)\">\n <div\n role=\"group\"\n [attr.aria-label]=\"groupName ? (groupName | syslang | transloco) : null\">\n <ul\n class=\"flex max-h-(--scroll-height,20rem) snap-y snap-start scrollbar-thin flex-col gap-0.5 overflow-auto pt-2\"\n role=\"list\">\n @for (option of dateOptions(); track option.display) {\n <li\n role=\"listitem\"\n [attr.aria-label]=\"\n maskedLabel(option) ?? (option.display | syslang | transloco)\n \"\n [class]=\"\n cn(\n itemStateClass,\n 'flex cursor-pointer items-center p-2 text-sm text-(--font-neutral-base) select-none',\n dateModel().option === option.display &&\n 'bg-(--bg-primary-lighter)',\n option.hidden && 'hidden',\n option.disabled && 'pointer-events-none opacity-40'\n )\n \"\n [attr.aria-hidden]=\"option.disabled\">\n <radio\n name=\"date-filter\"\n class=\"grow\"\n [checked]=\"dateModel().option === option.display\"\n [disabled]=\"option.disabled\"\n (checkedChange)=\"selectOption(option.display)\">\n {{\n maskedLabel(option) ?? (option.display | syslang | transloco)\n }}\n </radio>\n </li>\n }\n\n @if (allowCustomRange) {\n <li\n role=\"listitem\"\n aria-label=\"open date range picker\"\n [class]=\"\n cn(\n itemStateClass,\n 'flex cursor-pointer items-center gap-(--spacing-xs) p-2 text-sm text-(--font-neutral-base) select-none',\n dateModel().option === 'custom-range' &&\n 'bg-(--bg-primary-lighter)'\n )\n \">\n <radio\n name=\"date-filter\"\n class=\"grow items-center\"\n [checked]=\"dateModel().option === 'custom-range'\"\n (checkedChange)=\"selectOption('custom-range')\">\n <div class=\"flex flex-wrap items-center gap-x-1 gap-y-1\">\n <div class=\"flex items-center gap-1\">\n <label\n [attr.for]=\"startId\"\n class=\"radio-label min-w-8 truncate\"\n >{{ \"filters.from\" | transloco }}</label\n >\n <input-group\n class=\"h-8 max-w-[18ch] min-w-[18ch] has-[[readonly]]:border-(--stroke-primary-muted)\">\n <input\n input-control\n [id]=\"startId\"\n name=\"start\"\n type=\"text\"\n readonly\n [value]=\"customRangeFrom()\"\n (click)=\"selectAndOpenDialog()\"\n (keydown.enter)=\"onRangeInputEnter($event)\" />\n </input-group>\n </div>\n <div class=\"flex items-center gap-1\">\n <label\n [attr.for]=\"endId\"\n class=\"radio-label min-w-8 truncate text-right\"\n >{{ \"filters.to\" | transloco }}</label\n >\n <input-group\n class=\"h-8 max-w-[18ch] min-w-[18ch] has-[[readonly]]:border-(--stroke-primary-muted)\">\n <input\n input-control\n [id]=\"endId\"\n name=\"end\"\n type=\"text\"\n readonly\n [value]=\"customRangeTo()\"\n (click)=\"selectAndOpenDialog()\"\n (keydown.enter)=\"onRangeInputEnter($event)\" />\n </input-group>\n </div>\n </div>\n </radio>\n </li>\n }\n </ul>\n </div>\n </form>\n</details>\n",
23431
23557
  styles: [":host{min-width:200px}ul[role=list]{scrollbar-width:thin}\n"]
23432
23558
  }]
23433
23559
  }],
@@ -23565,6 +23691,10 @@ i0.ɵɵngDeclareClassMetadata({
23565
23691
  type: i0.Output,
23566
23692
  args: ["filtersChange"]
23567
23693
  }],
23694
+ aggregationOverridesChange: [{
23695
+ type: i0.Output,
23696
+ args: ["aggregationOverridesChange"]
23697
+ }],
23568
23698
  searchText: [{
23569
23699
  type: i0.Input,
23570
23700
  args: [{
@@ -26417,6 +26547,7 @@ var AggregationComponent = class AggregationComponent {
26417
26547
  "onClear",
26418
26548
  "selectionChange",
26419
26549
  "filtersChange",
26550
+ "aggregationOverridesChange",
26420
26551
  "searchTextChange"
26421
26552
  ]
26422
26553
  }
@@ -31045,11 +31176,6 @@ i0.ɵɵngDeclareClassMetadata({
31045
31176
  }]
31046
31177
  }] }
31047
31178
  });
31048
- const SUPPORTED_LANGUAGES = [
31049
- "en",
31050
- "fr",
31051
- "de"
31052
- ];
31053
31179
  const sameKeys = (a, b) => a.length === b.length && a.every((key, index) => key === b[index]);
31054
31180
  const userProfileSchema = (editing) => schema((p) => {
31055
31181
  applyEach(p.data, (field) => readonly(field, { when: ({ key }) => `data.${key()}` !== editing() }));
@@ -31057,7 +31183,6 @@ const userProfileSchema = (editing) => schema((p) => {
31057
31183
  });
31058
31184
  var UserProfileFormComponent = class UserProfileFormComponent {
31059
31185
  principalStore = inject(PrincipalStore);
31060
- userSettingsStore = inject(UserSettingsStore);
31061
31186
  userProfileService = inject(UserProfileService);
31062
31187
  appStore = inject(AppStore);
31063
31188
  client = inject(ATOMIC_CLIENT);
@@ -31105,26 +31230,6 @@ var UserProfileFormComponent = class UserProfileFormComponent {
31105
31230
  initials = computed(() => this.principalStore.initials(), ...ngDevMode ? [{ debugName: "initials" }] : /* istanbul ignore next */ []);
31106
31231
  username = computed(() => this.principalStore.name() || this.principalStore.email() || null, ...ngDevMode ? [{ debugName: "username" }] : /* istanbul ignore next */ []);
31107
31232
  propertyToEdit = signal(void 0, ...ngDevMode ? [{ debugName: "propertyToEdit" }] : /* istanbul ignore next */ []);
31108
- currentLanguage = model(null, ...ngDevMode ? [{ debugName: "currentLanguage" }] : /* istanbul ignore next */ []);
31109
- allLanguages = [
31110
- {
31111
- code: "en",
31112
- label: "English",
31113
- icon: FlagEnglishIcon
31114
- },
31115
- {
31116
- code: "fr",
31117
- label: "Français",
31118
- icon: FlagFrenchIcon
31119
- },
31120
- {
31121
- code: "de",
31122
- label: "Deutsch",
31123
- icon: FlagGermanIcon
31124
- }
31125
- ];
31126
- languageCodes = [...SUPPORTED_LANGUAGES];
31127
- languageLabel = (code) => this.allLanguages.find((lang) => lang.code === code)?.label ?? code;
31128
31233
  changingPassword = signal(false, ...ngDevMode ? [{ debugName: "changingPassword" }] : /* istanbul ignore next */ []);
31129
31234
  allowChangePassword = computed(() => {
31130
31235
  const useCredentials = this.client.config.authMode?.kind === "credentials";
@@ -31155,7 +31260,6 @@ var UserProfileFormComponent = class UserProfileFormComponent {
31155
31260
  submission: { action: async () => this.saveEditedProperty() }
31156
31261
  });
31157
31262
  constructor() {
31158
- this.currentLanguage.set(this.transloco.getActiveLang());
31159
31263
  effect(() => {
31160
31264
  if (this.userProfileResource.error()?.status === 404) this.createUserProfile();
31161
31265
  });
@@ -31293,17 +31397,6 @@ var UserProfileFormComponent = class UserProfileFormComponent {
31293
31397
  reader.readAsDataURL(element.files[0]);
31294
31398
  reader.onload = () => void this.patchProperty("data", "profilePhoto", reader.result);
31295
31399
  }
31296
- onLanguageChange(code) {
31297
- if (!code) return;
31298
- this.currentLanguage.set(code);
31299
- this.changeLanguage();
31300
- }
31301
- changeLanguage() {
31302
- const language = this.currentLanguage();
31303
- if (!language) return;
31304
- this.userSettingsStore.updateLanguage(language);
31305
- if (this.transloco.getActiveLang() !== language) this.transloco.setActiveLang(language);
31306
- }
31307
31400
  static ɵfac = i0.ɵɵngDeclareFactory({
31308
31401
  minVersion: "12.0.0",
31309
31402
  version: "22.2.1",
@@ -31318,18 +31411,10 @@ var UserProfileFormComponent = class UserProfileFormComponent {
31318
31411
  type: UserProfileFormComponent,
31319
31412
  isStandalone: true,
31320
31413
  selector: "user-profile-form, UserProfileForm, userprofileform",
31321
- inputs: { currentLanguage: {
31322
- classPropertyName: "currentLanguage",
31323
- publicName: "currentLanguage",
31324
- isSignal: true,
31325
- isRequired: false,
31326
- transformFunction: null
31327
- } },
31328
- outputs: { currentLanguage: "currentLanguageChange" },
31329
31414
  host: { classAttribute: "flex flex-col h-full overflow-hidden" },
31330
31415
  providers: [provideTranslocoScope("user-profile", "login")],
31331
31416
  ngImport: i0,
31332
- template: "@if (userProfileResource.hasValue()) {\n @let profile = user();\n <div class=\"flex min-h-0 flex-1 flex-col gap-2 p-4\">\n <div class=\"mb-4 flex\">\n @if (allowProfilePhoto()) {\n @let profilePhoto = profile?.data?.profilePhoto;\n <!-- AVATAR — a real <button>: the avatar opens the file picker, so it has to be reachable and\n activatable with the keyboard, which a click handler on a <div> never is. -->\n <button\n type=\"button\"\n variant=\"none\"\n class=\"cursor-pointer rounded-full\"\n [title]=\"'userProfile.changePhoto' | transloco\"\n [attr.aria-label]=\"'userProfile.changePhoto' | transloco\"\n (click)=\"avatarInput.click()\">\n <Avatar fill=\"secondary\" class=\"size-14\">\n @if (profilePhoto) {\n <!-- alt=\"\": the button already carries the accessible name, the image would repeat it. -->\n <AvatarImage [src]=\"profilePhoto\" width=\"44\" height=\"44\" alt=\"\" />\n }\n <AvatarFallback class=\"size-full text-lg\">\n @if (initials()) {\n <span>{{ initials() }}</span>\n } @else {\n <UserIcon class=\"size-7\" />\n }\n </AvatarFallback>\n </Avatar>\n </button>\n @if (profilePhoto) {\n <button\n type=\"button\"\n variant=\"tertiary\"\n [iconOnly]=\"true\"\n class=\"text-destructive cursor-pointer\"\n [title]=\"'userProfile.deleteProperty' | transloco\"\n [attr.aria-label]=\"\n 'userProfile.deletePropertyNamed'\n | transloco\n : { property: 'userProfile.data.profilePhoto' | transloco }\n \"\n (click)=\"onDeleteData('data', 'profilePhoto')\">\n <TrashIcon />\n </button>\n }\n }\n <div class=\"grow\"></div>\n <div class=\"flex flex-col gap-4\">\n <!-- LANGUAGE — outside the <form> below: it is a user setting applied on the spot, not a\n profile property submitted with the rest. The trigger is projected rather than left to the\n default one so it can carry an id, which is what makes the <label for> name the combobox —\n SelectMenu's own `fieldLabel` renders a <span>, which names nothing.\n The label goes in the `.select-label` slot: the DS wrap paints it (same size and weight as\n the property labels below) and provides the label-to-field gap, so this needs no wrapper. -->\n <SelectMenu\n class=\"w-fit\"\n size=\"md\"\n [options]=\"languageCodes\"\n [label]=\"languageLabel\"\n [value]=\"currentLanguage()\"\n (valueChange)=\"onLanguageChange($event)\">\n <label class=\"select-label\" for=\"user-profile-language\">{{\n \"userProfile.language\" | transloco\n }}</label>\n <button\n selectTrigger\n type=\"button\"\n id=\"user-profile-language\"\n class=\"select-trigger\">\n <span class=\"select-trigger-label\">{{\n languageLabel(currentLanguage() ?? \"\")\n }}</span>\n <chevron-down-icon class=\"select-chevron\" />\n </button>\n </SelectMenu>\n <!-- CHANGE PASSWORD -->\n @if (allowChangePassword()) {\n <button\n type=\"button\"\n [disabled]=\"changingPassword()\"\n (click)=\"changingPassword.set(true)\">\n {{ \"login.changePassword\" | transloco }}\n </button>\n }\n </div>\n </div>\n @if (changingPassword()) {\n <!-- ChangePassword renders its own <form [formRoot]>: it stays in this branch, mutually\n exclusive with the profile form below, so the two never nest. -->\n <ChangePassword\n [username]=\"username()\"\n [redirectAfterSuccess]=\"false\"\n [redirectAfterCancel]=\"false\"\n (cancel)=\"changingPassword.set(false)\"\n (success)=\"changingPassword.set(false)\" />\n } @else {\n <!-- p: the focus outline of the fields is drawn *outside* their box, and this is the\n scrolling ancestor — without a gutter it clips the ring on every edge it touches\n (left for all of them, bottom for the last one, top for the first). -->\n <div class=\"min-h-0 flex-1 overflow-auto p-(--spacing-3xs)\">\n <!--\n [formRoot] pose novalidate, intercepte submit et appelle preventDefault() avant de déclencher\n submit() sur le field tree. Un seul bouton type=\"submit\" est rendu à la fois — celui de la\n ligne en cours d'édition — et l'action de soumission persiste cette seule propriété.\n -->\n <!-- aria-label: a <form> with no accessible name is not exposed as a form landmark, so the\n fields could not be reached as a whole. aria-busy holds the announcement back while a\n property is being persisted. -->\n <form\n [formRoot]=\"profileForm\"\n [attr.aria-label]=\"'userProfile.profileFields' | transloco\"\n [attr.aria-busy]=\"profileForm().submitting() ? 'true' : null\">\n @for (\n property of properties();\n track property.category + \".\" + property.key\n ) {\n @let category = property.category;\n @let key = property.key;\n @let field = profileForm[category][key];\n @let label =\n property.translated\n ? (\"userProfile.data.\" + key | transloco)\n : key;\n @let editing = isEditing(category, key);\n @let invalid = showError(field);\n <!-- role=\"group\" named by the label: without it the four action buttons of a row are\n announced out of context, as a flat run of \"Edit …\" / \"Delete …\" with nothing tying\n them to the property they belong to. -->\n <div\n role=\"group\"\n [attr.aria-labelledby]=\"labelId(category, key)\"\n class=\"flex flex-col gap-(--spacing-3xs)\">\n <label\n [id]=\"labelId(category, key)\"\n [for]=\"fieldId(category, key)\"\n [class]=\"labelClass\"\n >{{ label }}</label\n >\n <div class=\"group flex flex-row\">\n <div class=\"min-w-0 grow\">\n <textarea-group\n resize=\"auto\"\n [maxRows]=\"3\"\n [scheme]=\"invalid ? 'error' : 'default'\">\n <!-- No [attr.readonly] here: [formField] writes the readonly attribute itself from\n the field state, which the schema's readonly() rule drives. The native\n readonly attribute is what exposes aria-readonly — do not duplicate it. -->\n <textarea\n textarea-control\n rows=\"1\"\n [id]=\"fieldId(category, key)\"\n [formField]=\"field\"\n [attr.placeholder]=\"'userProfile.notDefined' | transloco\"\n [attr.aria-invalid]=\"invalid ? 'true' : null\"\n [attr.aria-describedby]=\"\n invalid ? errorId(category, key) : null\n \"\n [attr.aria-keyshortcuts]=\"editing ? 'Escape' : null\"\n (focus)=\"onFieldFocus($event)\"\n (keydown.escape)=\"onCancelEdit(category, key)\"></textarea>\n </textarea-group>\n </div>\n <!-- group-focus-within, and not only group-hover: `invisible` takes the buttons out of\n the tab order, so without it editing or deleting a property is unreachable with a\n keyboard. Focusing the field of the row reveals its actions. -->\n @if (editing) {\n <button\n type=\"submit\"\n variant=\"tertiary\"\n [iconOnly]=\"true\"\n class=\"visible mx-2 cursor-pointer sm:invisible sm:group-focus-within:visible sm:group-hover:visible\"\n [disabled]=\"profileForm().submitting()\"\n [title]=\"'userProfile.saveProperty' | transloco\"\n [attr.aria-label]=\"\n 'userProfile.savePropertyNamed'\n | transloco: { property: label }\n \">\n <save-icon />\n </button>\n <button\n type=\"button\"\n variant=\"tertiary\"\n [iconOnly]=\"true\"\n class=\"visible cursor-pointer sm:invisible sm:group-focus-within:visible sm:group-hover:visible\"\n [title]=\"'userProfile.cancelEdit' | transloco\"\n [attr.aria-label]=\"\n 'userProfile.cancelEditNamed'\n | transloco: { property: label }\n \"\n (click)=\"onCancelEdit(category, key)\">\n <UndoIcon />\n </button>\n } @else {\n <button\n type=\"button\"\n variant=\"tertiary\"\n [iconOnly]=\"true\"\n [id]=\"editButtonId(category, key)\"\n class=\"visible mx-2 cursor-pointer sm:invisible sm:group-focus-within:visible sm:group-hover:visible\"\n [title]=\"'userProfile.editProperty' | transloco\"\n [attr.aria-label]=\"\n 'userProfile.editPropertyNamed'\n | transloco: { property: label }\n \"\n (click)=\"onEdit(category, key)\">\n <EditIcon />\n </button>\n <button\n type=\"button\"\n variant=\"tertiary\"\n [iconOnly]=\"true\"\n class=\"text-destructive visible cursor-pointer sm:invisible sm:group-focus-within:visible sm:group-hover:visible\"\n [title]=\"'userProfile.deleteProperty' | transloco\"\n [attr.aria-label]=\"\n 'userProfile.deletePropertyNamed'\n | transloco: { property: label }\n \"\n (click)=\"onDeleteData(category, key)\">\n <TrashIcon />\n </button>\n }\n </div>\n @if (invalid) {\n <p\n [id]=\"errorId(category, key)\"\n role=\"alert\"\n [class]=\"errorClass\">\n {{ errorKey(field) | transloco }}\n </p>\n }\n </div>\n }\n </form>\n <!-- Always in the DOM, and only its text changes: a live region inserted at the same time as\n its content is not reliably announced. `sr-only` — the transitions it describes are\n already visible on screen. -->\n <p class=\"sr-only\" role=\"status\" aria-live=\"polite\">\n {{ statusMessage() }}\n </p>\n </div>\n }\n </div>\n} @else if (userProfileResource.isLoading()) {\n <p role=\"status\">{{ \"userProfile.loading\" | transloco }}</p>\n} @else if (userProfileResource.error()) {\n <p role=\"alert\">{{ \"userProfile.loadError\" | transloco }}</p>\n}\n\n<!-- AVATAR UPLOAD — `hidden` keeps it out of both the tab order and the accessibility tree, which is\n intended: it is opened by the avatar button above. It also sits outside the <form>. -->\n<input\n #avatarInput\n class=\"hidden\"\n type=\"file\"\n accept=\"image/*\"\n (change)=\"uploadAvatar($event)\" />\n",
31417
+ template: "@if (userProfileResource.hasValue()) {\n @let profile = user();\n <div class=\"flex min-h-0 flex-1 flex-col gap-2 p-4\">\n <div class=\"mb-4 flex\">\n @if (allowProfilePhoto()) {\n @let profilePhoto = profile?.data?.profilePhoto;\n <!-- AVATAR — a real <button>: the avatar opens the file picker, so it has to be reachable and\n activatable with the keyboard, which a click handler on a <div> never is. -->\n <button\n type=\"button\"\n variant=\"none\"\n class=\"cursor-pointer rounded-full\"\n [title]=\"'userProfile.changePhoto' | transloco\"\n [attr.aria-label]=\"'userProfile.changePhoto' | transloco\"\n (click)=\"avatarInput.click()\">\n <Avatar fill=\"secondary\" class=\"size-14\">\n @if (profilePhoto) {\n <!-- alt=\"\": the button already carries the accessible name, the image would repeat it. -->\n <AvatarImage [src]=\"profilePhoto\" width=\"44\" height=\"44\" alt=\"\" />\n }\n <AvatarFallback class=\"size-full text-lg\">\n @if (initials()) {\n <span>{{ initials() }}</span>\n } @else {\n <UserIcon class=\"size-7\" />\n }\n </AvatarFallback>\n </Avatar>\n </button>\n @if (profilePhoto) {\n <button\n type=\"button\"\n variant=\"tertiary\"\n [iconOnly]=\"true\"\n class=\"text-destructive cursor-pointer\"\n [title]=\"'userProfile.deleteProperty' | transloco\"\n [attr.aria-label]=\"\n 'userProfile.deletePropertyNamed'\n | transloco\n : { property: 'userProfile.data.profilePhoto' | transloco }\n \"\n (click)=\"onDeleteData('data', 'profilePhoto')\">\n <TrashIcon />\n </button>\n }\n }\n <div class=\"grow\"></div>\n <div class=\"flex flex-col gap-4\">\n <!-- CHANGE PASSWORD -->\n @if (allowChangePassword()) {\n <button\n type=\"button\"\n [disabled]=\"changingPassword()\"\n (click)=\"changingPassword.set(true)\">\n {{ \"login.changePassword\" | transloco }}\n </button>\n }\n </div>\n </div>\n @if (changingPassword()) {\n <!-- ChangePassword renders its own <form [formRoot]>: it stays in this branch, mutually\n exclusive with the profile form below, so the two never nest. -->\n <ChangePassword\n [username]=\"username()\"\n [redirectAfterSuccess]=\"false\"\n [redirectAfterCancel]=\"false\"\n (cancel)=\"changingPassword.set(false)\"\n (success)=\"changingPassword.set(false)\" />\n } @else {\n <!-- p: the focus outline of the fields is drawn *outside* their box, and this is the\n scrolling ancestor — without a gutter it clips the ring on every edge it touches\n (left for all of them, bottom for the last one, top for the first). -->\n <div class=\"min-h-0 flex-1 overflow-auto p-(--spacing-3xs)\">\n <!--\n [formRoot] pose novalidate, intercepte submit et appelle preventDefault() avant de déclencher\n submit() sur le field tree. Un seul bouton type=\"submit\" est rendu à la fois — celui de la\n ligne en cours d'édition — et l'action de soumission persiste cette seule propriété.\n -->\n <!-- aria-label: a <form> with no accessible name is not exposed as a form landmark, so the\n fields could not be reached as a whole. aria-busy holds the announcement back while a\n property is being persisted. -->\n <form\n [formRoot]=\"profileForm\"\n [attr.aria-label]=\"'userProfile.profileFields' | transloco\"\n [attr.aria-busy]=\"profileForm().submitting() ? 'true' : null\">\n @for (\n property of properties();\n track property.category + \".\" + property.key\n ) {\n @let category = property.category;\n @let key = property.key;\n @let field = profileForm[category][key];\n @let label =\n property.translated\n ? (\"userProfile.data.\" + key | transloco)\n : key;\n @let editing = isEditing(category, key);\n @let invalid = showError(field);\n <!-- role=\"group\" named by the label: without it the four action buttons of a row are\n announced out of context, as a flat run of \"Edit …\" / \"Delete …\" with nothing tying\n them to the property they belong to. -->\n <div\n role=\"group\"\n [attr.aria-labelledby]=\"labelId(category, key)\"\n class=\"flex flex-col gap-(--spacing-3xs)\">\n <label\n [id]=\"labelId(category, key)\"\n [for]=\"fieldId(category, key)\"\n [class]=\"labelClass\"\n >{{ label }}</label\n >\n <div class=\"group flex flex-row\">\n <div class=\"min-w-0 grow\">\n <textarea-group\n resize=\"auto\"\n [maxRows]=\"3\"\n [scheme]=\"invalid ? 'error' : 'default'\">\n <!-- No [attr.readonly] here: [formField] writes the readonly attribute itself from\n the field state, which the schema's readonly() rule drives. The native\n readonly attribute is what exposes aria-readonly — do not duplicate it. -->\n <textarea\n textarea-control\n rows=\"1\"\n [id]=\"fieldId(category, key)\"\n [formField]=\"field\"\n [attr.placeholder]=\"'userProfile.notDefined' | transloco\"\n [attr.aria-invalid]=\"invalid ? 'true' : null\"\n [attr.aria-describedby]=\"\n invalid ? errorId(category, key) : null\n \"\n [attr.aria-keyshortcuts]=\"editing ? 'Escape' : null\"\n (focus)=\"onFieldFocus($event)\"\n (keydown.escape)=\"onCancelEdit(category, key)\"></textarea>\n </textarea-group>\n </div>\n <!-- group-focus-within, and not only group-hover: `invisible` takes the buttons out of\n the tab order, so without it editing or deleting a property is unreachable with a\n keyboard. Focusing the field of the row reveals its actions. -->\n @if (editing) {\n <button\n type=\"submit\"\n variant=\"tertiary\"\n [iconOnly]=\"true\"\n class=\"visible mx-2 cursor-pointer sm:invisible sm:group-focus-within:visible sm:group-hover:visible\"\n [disabled]=\"profileForm().submitting()\"\n [title]=\"'userProfile.saveProperty' | transloco\"\n [attr.aria-label]=\"\n 'userProfile.savePropertyNamed'\n | transloco: { property: label }\n \">\n <save-icon />\n </button>\n <button\n type=\"button\"\n variant=\"tertiary\"\n [iconOnly]=\"true\"\n class=\"visible cursor-pointer sm:invisible sm:group-focus-within:visible sm:group-hover:visible\"\n [title]=\"'userProfile.cancelEdit' | transloco\"\n [attr.aria-label]=\"\n 'userProfile.cancelEditNamed'\n | transloco: { property: label }\n \"\n (click)=\"onCancelEdit(category, key)\">\n <UndoIcon />\n </button>\n } @else {\n <button\n type=\"button\"\n variant=\"tertiary\"\n [iconOnly]=\"true\"\n [id]=\"editButtonId(category, key)\"\n class=\"visible mx-2 cursor-pointer sm:invisible sm:group-focus-within:visible sm:group-hover:visible\"\n [title]=\"'userProfile.editProperty' | transloco\"\n [attr.aria-label]=\"\n 'userProfile.editPropertyNamed'\n | transloco: { property: label }\n \"\n (click)=\"onEdit(category, key)\">\n <EditIcon />\n </button>\n <button\n type=\"button\"\n variant=\"tertiary\"\n [iconOnly]=\"true\"\n class=\"text-destructive visible cursor-pointer sm:invisible sm:group-focus-within:visible sm:group-hover:visible\"\n [title]=\"'userProfile.deleteProperty' | transloco\"\n [attr.aria-label]=\"\n 'userProfile.deletePropertyNamed'\n | transloco: { property: label }\n \"\n (click)=\"onDeleteData(category, key)\">\n <TrashIcon />\n </button>\n }\n </div>\n @if (invalid) {\n <p\n [id]=\"errorId(category, key)\"\n role=\"alert\"\n [class]=\"errorClass\">\n {{ errorKey(field) | transloco }}\n </p>\n }\n </div>\n }\n </form>\n <!-- Always in the DOM, and only its text changes: a live region inserted at the same time as\n its content is not reliably announced. `sr-only` — the transitions it describes are\n already visible on screen. -->\n <p class=\"sr-only\" role=\"status\" aria-live=\"polite\">\n {{ statusMessage() }}\n </p>\n </div>\n }\n </div>\n} @else if (userProfileResource.isLoading()) {\n <p role=\"status\">{{ \"userProfile.loading\" | transloco }}</p>\n} @else if (userProfileResource.error()) {\n <p role=\"alert\">{{ \"userProfile.loadError\" | transloco }}</p>\n}\n\n<!-- AVATAR UPLOAD — `hidden` keeps it out of both the tab order and the accessibility tree, which is\n intended: it is opened by the avatar button above. It also sits outside the <form>. -->\n<input\n #avatarInput\n class=\"hidden\"\n type=\"file\"\n accept=\"image/*\"\n (change)=\"uploadAvatar($event)\" />\n",
31333
31418
  dependencies: [
31334
31419
  {
31335
31420
  kind: "directive",
@@ -31422,45 +31507,6 @@ var UserProfileFormComponent = class UserProfileFormComponent {
31422
31507
  "size"
31423
31508
  ]
31424
31509
  },
31425
- {
31426
- kind: "component",
31427
- type: SelectComponent,
31428
- selector: "SelectMenu,select-menu",
31429
- inputs: [
31430
- "value",
31431
- "options",
31432
- "placement",
31433
- "size",
31434
- "fieldLabel",
31435
- "aria-label",
31436
- "placeholder",
31437
- "hint",
31438
- "disabled",
31439
- "class",
31440
- "selectionMode",
31441
- "multiple",
31442
- "wrap",
31443
- "key",
31444
- "label",
31445
- "optionDisabled",
31446
- "noOptionsText",
31447
- "contentClass"
31448
- ],
31449
- outputs: ["valueChange", "touch"],
31450
- exportAs: ["select"]
31451
- },
31452
- {
31453
- kind: "directive",
31454
- type: SelectTriggerDirective,
31455
- selector: "[selectTrigger]",
31456
- exportAs: ["selectTrigger"]
31457
- },
31458
- {
31459
- kind: "component",
31460
- type: ChevronDownIcon,
31461
- selector: "chevron-down, chevron-down-icon, ChevronDown, ChevronDownIcon",
31462
- inputs: ["class"]
31463
- },
31464
31510
  {
31465
31511
  kind: "directive",
31466
31512
  type: TextareaComponent,
@@ -31530,29 +31576,15 @@ i0.ɵɵngDeclareClassMetadata({
31530
31576
  UserIcon,
31531
31577
  AvatarImageComponent,
31532
31578
  ButtonComponent,
31533
- SelectComponent,
31534
- SelectTriggerDirective,
31535
- ChevronDownIcon,
31536
31579
  TextareaComponent,
31537
31580
  TextareaControlDirective,
31538
31581
  ChangePasswordComponent
31539
31582
  ],
31540
31583
  providers: [provideTranslocoScope("user-profile", "login")],
31541
- template: "@if (userProfileResource.hasValue()) {\n @let profile = user();\n <div class=\"flex min-h-0 flex-1 flex-col gap-2 p-4\">\n <div class=\"mb-4 flex\">\n @if (allowProfilePhoto()) {\n @let profilePhoto = profile?.data?.profilePhoto;\n <!-- AVATAR — a real <button>: the avatar opens the file picker, so it has to be reachable and\n activatable with the keyboard, which a click handler on a <div> never is. -->\n <button\n type=\"button\"\n variant=\"none\"\n class=\"cursor-pointer rounded-full\"\n [title]=\"'userProfile.changePhoto' | transloco\"\n [attr.aria-label]=\"'userProfile.changePhoto' | transloco\"\n (click)=\"avatarInput.click()\">\n <Avatar fill=\"secondary\" class=\"size-14\">\n @if (profilePhoto) {\n <!-- alt=\"\": the button already carries the accessible name, the image would repeat it. -->\n <AvatarImage [src]=\"profilePhoto\" width=\"44\" height=\"44\" alt=\"\" />\n }\n <AvatarFallback class=\"size-full text-lg\">\n @if (initials()) {\n <span>{{ initials() }}</span>\n } @else {\n <UserIcon class=\"size-7\" />\n }\n </AvatarFallback>\n </Avatar>\n </button>\n @if (profilePhoto) {\n <button\n type=\"button\"\n variant=\"tertiary\"\n [iconOnly]=\"true\"\n class=\"text-destructive cursor-pointer\"\n [title]=\"'userProfile.deleteProperty' | transloco\"\n [attr.aria-label]=\"\n 'userProfile.deletePropertyNamed'\n | transloco\n : { property: 'userProfile.data.profilePhoto' | transloco }\n \"\n (click)=\"onDeleteData('data', 'profilePhoto')\">\n <TrashIcon />\n </button>\n }\n }\n <div class=\"grow\"></div>\n <div class=\"flex flex-col gap-4\">\n <!-- LANGUAGE — outside the <form> below: it is a user setting applied on the spot, not a\n profile property submitted with the rest. The trigger is projected rather than left to the\n default one so it can carry an id, which is what makes the <label for> name the combobox —\n SelectMenu's own `fieldLabel` renders a <span>, which names nothing.\n The label goes in the `.select-label` slot: the DS wrap paints it (same size and weight as\n the property labels below) and provides the label-to-field gap, so this needs no wrapper. -->\n <SelectMenu\n class=\"w-fit\"\n size=\"md\"\n [options]=\"languageCodes\"\n [label]=\"languageLabel\"\n [value]=\"currentLanguage()\"\n (valueChange)=\"onLanguageChange($event)\">\n <label class=\"select-label\" for=\"user-profile-language\">{{\n \"userProfile.language\" | transloco\n }}</label>\n <button\n selectTrigger\n type=\"button\"\n id=\"user-profile-language\"\n class=\"select-trigger\">\n <span class=\"select-trigger-label\">{{\n languageLabel(currentLanguage() ?? \"\")\n }}</span>\n <chevron-down-icon class=\"select-chevron\" />\n </button>\n </SelectMenu>\n <!-- CHANGE PASSWORD -->\n @if (allowChangePassword()) {\n <button\n type=\"button\"\n [disabled]=\"changingPassword()\"\n (click)=\"changingPassword.set(true)\">\n {{ \"login.changePassword\" | transloco }}\n </button>\n }\n </div>\n </div>\n @if (changingPassword()) {\n <!-- ChangePassword renders its own <form [formRoot]>: it stays in this branch, mutually\n exclusive with the profile form below, so the two never nest. -->\n <ChangePassword\n [username]=\"username()\"\n [redirectAfterSuccess]=\"false\"\n [redirectAfterCancel]=\"false\"\n (cancel)=\"changingPassword.set(false)\"\n (success)=\"changingPassword.set(false)\" />\n } @else {\n <!-- p: the focus outline of the fields is drawn *outside* their box, and this is the\n scrolling ancestor — without a gutter it clips the ring on every edge it touches\n (left for all of them, bottom for the last one, top for the first). -->\n <div class=\"min-h-0 flex-1 overflow-auto p-(--spacing-3xs)\">\n <!--\n [formRoot] pose novalidate, intercepte submit et appelle preventDefault() avant de déclencher\n submit() sur le field tree. Un seul bouton type=\"submit\" est rendu à la fois — celui de la\n ligne en cours d'édition — et l'action de soumission persiste cette seule propriété.\n -->\n <!-- aria-label: a <form> with no accessible name is not exposed as a form landmark, so the\n fields could not be reached as a whole. aria-busy holds the announcement back while a\n property is being persisted. -->\n <form\n [formRoot]=\"profileForm\"\n [attr.aria-label]=\"'userProfile.profileFields' | transloco\"\n [attr.aria-busy]=\"profileForm().submitting() ? 'true' : null\">\n @for (\n property of properties();\n track property.category + \".\" + property.key\n ) {\n @let category = property.category;\n @let key = property.key;\n @let field = profileForm[category][key];\n @let label =\n property.translated\n ? (\"userProfile.data.\" + key | transloco)\n : key;\n @let editing = isEditing(category, key);\n @let invalid = showError(field);\n <!-- role=\"group\" named by the label: without it the four action buttons of a row are\n announced out of context, as a flat run of \"Edit …\" / \"Delete …\" with nothing tying\n them to the property they belong to. -->\n <div\n role=\"group\"\n [attr.aria-labelledby]=\"labelId(category, key)\"\n class=\"flex flex-col gap-(--spacing-3xs)\">\n <label\n [id]=\"labelId(category, key)\"\n [for]=\"fieldId(category, key)\"\n [class]=\"labelClass\"\n >{{ label }}</label\n >\n <div class=\"group flex flex-row\">\n <div class=\"min-w-0 grow\">\n <textarea-group\n resize=\"auto\"\n [maxRows]=\"3\"\n [scheme]=\"invalid ? 'error' : 'default'\">\n <!-- No [attr.readonly] here: [formField] writes the readonly attribute itself from\n the field state, which the schema's readonly() rule drives. The native\n readonly attribute is what exposes aria-readonly — do not duplicate it. -->\n <textarea\n textarea-control\n rows=\"1\"\n [id]=\"fieldId(category, key)\"\n [formField]=\"field\"\n [attr.placeholder]=\"'userProfile.notDefined' | transloco\"\n [attr.aria-invalid]=\"invalid ? 'true' : null\"\n [attr.aria-describedby]=\"\n invalid ? errorId(category, key) : null\n \"\n [attr.aria-keyshortcuts]=\"editing ? 'Escape' : null\"\n (focus)=\"onFieldFocus($event)\"\n (keydown.escape)=\"onCancelEdit(category, key)\"></textarea>\n </textarea-group>\n </div>\n <!-- group-focus-within, and not only group-hover: `invisible` takes the buttons out of\n the tab order, so without it editing or deleting a property is unreachable with a\n keyboard. Focusing the field of the row reveals its actions. -->\n @if (editing) {\n <button\n type=\"submit\"\n variant=\"tertiary\"\n [iconOnly]=\"true\"\n class=\"visible mx-2 cursor-pointer sm:invisible sm:group-focus-within:visible sm:group-hover:visible\"\n [disabled]=\"profileForm().submitting()\"\n [title]=\"'userProfile.saveProperty' | transloco\"\n [attr.aria-label]=\"\n 'userProfile.savePropertyNamed'\n | transloco: { property: label }\n \">\n <save-icon />\n </button>\n <button\n type=\"button\"\n variant=\"tertiary\"\n [iconOnly]=\"true\"\n class=\"visible cursor-pointer sm:invisible sm:group-focus-within:visible sm:group-hover:visible\"\n [title]=\"'userProfile.cancelEdit' | transloco\"\n [attr.aria-label]=\"\n 'userProfile.cancelEditNamed'\n | transloco: { property: label }\n \"\n (click)=\"onCancelEdit(category, key)\">\n <UndoIcon />\n </button>\n } @else {\n <button\n type=\"button\"\n variant=\"tertiary\"\n [iconOnly]=\"true\"\n [id]=\"editButtonId(category, key)\"\n class=\"visible mx-2 cursor-pointer sm:invisible sm:group-focus-within:visible sm:group-hover:visible\"\n [title]=\"'userProfile.editProperty' | transloco\"\n [attr.aria-label]=\"\n 'userProfile.editPropertyNamed'\n | transloco: { property: label }\n \"\n (click)=\"onEdit(category, key)\">\n <EditIcon />\n </button>\n <button\n type=\"button\"\n variant=\"tertiary\"\n [iconOnly]=\"true\"\n class=\"text-destructive visible cursor-pointer sm:invisible sm:group-focus-within:visible sm:group-hover:visible\"\n [title]=\"'userProfile.deleteProperty' | transloco\"\n [attr.aria-label]=\"\n 'userProfile.deletePropertyNamed'\n | transloco: { property: label }\n \"\n (click)=\"onDeleteData(category, key)\">\n <TrashIcon />\n </button>\n }\n </div>\n @if (invalid) {\n <p\n [id]=\"errorId(category, key)\"\n role=\"alert\"\n [class]=\"errorClass\">\n {{ errorKey(field) | transloco }}\n </p>\n }\n </div>\n }\n </form>\n <!-- Always in the DOM, and only its text changes: a live region inserted at the same time as\n its content is not reliably announced. `sr-only` — the transitions it describes are\n already visible on screen. -->\n <p class=\"sr-only\" role=\"status\" aria-live=\"polite\">\n {{ statusMessage() }}\n </p>\n </div>\n }\n </div>\n} @else if (userProfileResource.isLoading()) {\n <p role=\"status\">{{ \"userProfile.loading\" | transloco }}</p>\n} @else if (userProfileResource.error()) {\n <p role=\"alert\">{{ \"userProfile.loadError\" | transloco }}</p>\n}\n\n<!-- AVATAR UPLOAD — `hidden` keeps it out of both the tab order and the accessibility tree, which is\n intended: it is opened by the avatar button above. It also sits outside the <form>. -->\n<input\n #avatarInput\n class=\"hidden\"\n type=\"file\"\n accept=\"image/*\"\n (change)=\"uploadAvatar($event)\" />\n"
31584
+ template: "@if (userProfileResource.hasValue()) {\n @let profile = user();\n <div class=\"flex min-h-0 flex-1 flex-col gap-2 p-4\">\n <div class=\"mb-4 flex\">\n @if (allowProfilePhoto()) {\n @let profilePhoto = profile?.data?.profilePhoto;\n <!-- AVATAR — a real <button>: the avatar opens the file picker, so it has to be reachable and\n activatable with the keyboard, which a click handler on a <div> never is. -->\n <button\n type=\"button\"\n variant=\"none\"\n class=\"cursor-pointer rounded-full\"\n [title]=\"'userProfile.changePhoto' | transloco\"\n [attr.aria-label]=\"'userProfile.changePhoto' | transloco\"\n (click)=\"avatarInput.click()\">\n <Avatar fill=\"secondary\" class=\"size-14\">\n @if (profilePhoto) {\n <!-- alt=\"\": the button already carries the accessible name, the image would repeat it. -->\n <AvatarImage [src]=\"profilePhoto\" width=\"44\" height=\"44\" alt=\"\" />\n }\n <AvatarFallback class=\"size-full text-lg\">\n @if (initials()) {\n <span>{{ initials() }}</span>\n } @else {\n <UserIcon class=\"size-7\" />\n }\n </AvatarFallback>\n </Avatar>\n </button>\n @if (profilePhoto) {\n <button\n type=\"button\"\n variant=\"tertiary\"\n [iconOnly]=\"true\"\n class=\"text-destructive cursor-pointer\"\n [title]=\"'userProfile.deleteProperty' | transloco\"\n [attr.aria-label]=\"\n 'userProfile.deletePropertyNamed'\n | transloco\n : { property: 'userProfile.data.profilePhoto' | transloco }\n \"\n (click)=\"onDeleteData('data', 'profilePhoto')\">\n <TrashIcon />\n </button>\n }\n }\n <div class=\"grow\"></div>\n <div class=\"flex flex-col gap-4\">\n <!-- CHANGE PASSWORD -->\n @if (allowChangePassword()) {\n <button\n type=\"button\"\n [disabled]=\"changingPassword()\"\n (click)=\"changingPassword.set(true)\">\n {{ \"login.changePassword\" | transloco }}\n </button>\n }\n </div>\n </div>\n @if (changingPassword()) {\n <!-- ChangePassword renders its own <form [formRoot]>: it stays in this branch, mutually\n exclusive with the profile form below, so the two never nest. -->\n <ChangePassword\n [username]=\"username()\"\n [redirectAfterSuccess]=\"false\"\n [redirectAfterCancel]=\"false\"\n (cancel)=\"changingPassword.set(false)\"\n (success)=\"changingPassword.set(false)\" />\n } @else {\n <!-- p: the focus outline of the fields is drawn *outside* their box, and this is the\n scrolling ancestor — without a gutter it clips the ring on every edge it touches\n (left for all of them, bottom for the last one, top for the first). -->\n <div class=\"min-h-0 flex-1 overflow-auto p-(--spacing-3xs)\">\n <!--\n [formRoot] pose novalidate, intercepte submit et appelle preventDefault() avant de déclencher\n submit() sur le field tree. Un seul bouton type=\"submit\" est rendu à la fois — celui de la\n ligne en cours d'édition — et l'action de soumission persiste cette seule propriété.\n -->\n <!-- aria-label: a <form> with no accessible name is not exposed as a form landmark, so the\n fields could not be reached as a whole. aria-busy holds the announcement back while a\n property is being persisted. -->\n <form\n [formRoot]=\"profileForm\"\n [attr.aria-label]=\"'userProfile.profileFields' | transloco\"\n [attr.aria-busy]=\"profileForm().submitting() ? 'true' : null\">\n @for (\n property of properties();\n track property.category + \".\" + property.key\n ) {\n @let category = property.category;\n @let key = property.key;\n @let field = profileForm[category][key];\n @let label =\n property.translated\n ? (\"userProfile.data.\" + key | transloco)\n : key;\n @let editing = isEditing(category, key);\n @let invalid = showError(field);\n <!-- role=\"group\" named by the label: without it the four action buttons of a row are\n announced out of context, as a flat run of \"Edit …\" / \"Delete …\" with nothing tying\n them to the property they belong to. -->\n <div\n role=\"group\"\n [attr.aria-labelledby]=\"labelId(category, key)\"\n class=\"flex flex-col gap-(--spacing-3xs)\">\n <label\n [id]=\"labelId(category, key)\"\n [for]=\"fieldId(category, key)\"\n [class]=\"labelClass\"\n >{{ label }}</label\n >\n <div class=\"group flex flex-row\">\n <div class=\"min-w-0 grow\">\n <textarea-group\n resize=\"auto\"\n [maxRows]=\"3\"\n [scheme]=\"invalid ? 'error' : 'default'\">\n <!-- No [attr.readonly] here: [formField] writes the readonly attribute itself from\n the field state, which the schema's readonly() rule drives. The native\n readonly attribute is what exposes aria-readonly — do not duplicate it. -->\n <textarea\n textarea-control\n rows=\"1\"\n [id]=\"fieldId(category, key)\"\n [formField]=\"field\"\n [attr.placeholder]=\"'userProfile.notDefined' | transloco\"\n [attr.aria-invalid]=\"invalid ? 'true' : null\"\n [attr.aria-describedby]=\"\n invalid ? errorId(category, key) : null\n \"\n [attr.aria-keyshortcuts]=\"editing ? 'Escape' : null\"\n (focus)=\"onFieldFocus($event)\"\n (keydown.escape)=\"onCancelEdit(category, key)\"></textarea>\n </textarea-group>\n </div>\n <!-- group-focus-within, and not only group-hover: `invisible` takes the buttons out of\n the tab order, so without it editing or deleting a property is unreachable with a\n keyboard. Focusing the field of the row reveals its actions. -->\n @if (editing) {\n <button\n type=\"submit\"\n variant=\"tertiary\"\n [iconOnly]=\"true\"\n class=\"visible mx-2 cursor-pointer sm:invisible sm:group-focus-within:visible sm:group-hover:visible\"\n [disabled]=\"profileForm().submitting()\"\n [title]=\"'userProfile.saveProperty' | transloco\"\n [attr.aria-label]=\"\n 'userProfile.savePropertyNamed'\n | transloco: { property: label }\n \">\n <save-icon />\n </button>\n <button\n type=\"button\"\n variant=\"tertiary\"\n [iconOnly]=\"true\"\n class=\"visible cursor-pointer sm:invisible sm:group-focus-within:visible sm:group-hover:visible\"\n [title]=\"'userProfile.cancelEdit' | transloco\"\n [attr.aria-label]=\"\n 'userProfile.cancelEditNamed'\n | transloco: { property: label }\n \"\n (click)=\"onCancelEdit(category, key)\">\n <UndoIcon />\n </button>\n } @else {\n <button\n type=\"button\"\n variant=\"tertiary\"\n [iconOnly]=\"true\"\n [id]=\"editButtonId(category, key)\"\n class=\"visible mx-2 cursor-pointer sm:invisible sm:group-focus-within:visible sm:group-hover:visible\"\n [title]=\"'userProfile.editProperty' | transloco\"\n [attr.aria-label]=\"\n 'userProfile.editPropertyNamed'\n | transloco: { property: label }\n \"\n (click)=\"onEdit(category, key)\">\n <EditIcon />\n </button>\n <button\n type=\"button\"\n variant=\"tertiary\"\n [iconOnly]=\"true\"\n class=\"text-destructive visible cursor-pointer sm:invisible sm:group-focus-within:visible sm:group-hover:visible\"\n [title]=\"'userProfile.deleteProperty' | transloco\"\n [attr.aria-label]=\"\n 'userProfile.deletePropertyNamed'\n | transloco: { property: label }\n \"\n (click)=\"onDeleteData(category, key)\">\n <TrashIcon />\n </button>\n }\n </div>\n @if (invalid) {\n <p\n [id]=\"errorId(category, key)\"\n role=\"alert\"\n [class]=\"errorClass\">\n {{ errorKey(field) | transloco }}\n </p>\n }\n </div>\n }\n </form>\n <!-- Always in the DOM, and only its text changes: a live region inserted at the same time as\n its content is not reliably announced. `sr-only` — the transitions it describes are\n already visible on screen. -->\n <p class=\"sr-only\" role=\"status\" aria-live=\"polite\">\n {{ statusMessage() }}\n </p>\n </div>\n }\n </div>\n} @else if (userProfileResource.isLoading()) {\n <p role=\"status\">{{ \"userProfile.loading\" | transloco }}</p>\n} @else if (userProfileResource.error()) {\n <p role=\"alert\">{{ \"userProfile.loadError\" | transloco }}</p>\n}\n\n<!-- AVATAR UPLOAD — `hidden` keeps it out of both the tab order and the accessibility tree, which is\n intended: it is opened by the avatar button above. It also sits outside the <form>. -->\n<input\n #avatarInput\n class=\"hidden\"\n type=\"file\"\n accept=\"image/*\"\n (change)=\"uploadAvatar($event)\" />\n"
31542
31585
  }]
31543
31586
  }],
31544
- ctorParameters: () => [],
31545
- propDecorators: { currentLanguage: [{
31546
- type: i0.Input,
31547
- args: [{
31548
- isSignal: true,
31549
- alias: "currentLanguage",
31550
- required: false
31551
- }]
31552
- }, {
31553
- type: i0.Output,
31554
- args: ["currentLanguageChange"]
31555
- }] }
31587
+ ctorParameters: () => []
31556
31588
  });
31557
31589
  function toRecord(keys, source) {
31558
31590
  return Object.fromEntries(keys.map((key) => [key, String(source?.[key] ?? "")]));
@@ -31564,63 +31596,683 @@ function splitPropertyId(id) {
31564
31596
  key: id.slice(separator + 1)
31565
31597
  };
31566
31598
  }
31567
- var UserProfileDialog = class UserProfileDialog {
31568
- call = injectCallRef();
31569
- dialog = viewChild.required(DialogComponent, ...ngDevMode ? [{ debugName: "dialog" }] : /* istanbul ignore next */ []);
31570
- constructor() {
31571
- afterNextRender(() => this.dialog().open());
31599
+ var SettingsAccountPanelComponent = class SettingsAccountPanelComponent {
31600
+ principalStore = inject(PrincipalStore);
31601
+ appStore = inject(AppStore);
31602
+ client = inject(ATOMIC_CLIENT);
31603
+ userOverride = inject(UserOverrideService);
31604
+ injector = inject(Injector);
31605
+ profileEnabled = computed(() => this.appStore.general()?.features?.userProfile?.enabled ?? false, ...ngDevMode ? [{ debugName: "profileEnabled" }] : /* istanbul ignore next */ []);
31606
+ username = computed(() => this.principalStore.name() || this.principalStore.email() || null, ...ngDevMode ? [{ debugName: "username" }] : /* istanbul ignore next */ []);
31607
+ allowChangePassword = computed(() => {
31608
+ const useCredentials = this.client.config.authMode?.kind === "credentials";
31609
+ const { allowChangePassword = false } = this.appStore.general()?.features || {};
31610
+ return allowChangePassword && useCredentials && (this.principalStore.editablePartition?.() || false);
31611
+ }, ...ngDevMode ? [{ debugName: "allowChangePassword" }] : /* istanbul ignore next */ []);
31612
+ resetSettings() {
31613
+ ResetUserSettings.call(void 0, { injector: this.injector });
31614
+ }
31615
+ overrideUser() {
31616
+ OverrideUser.call(void 0, { injector: this.injector });
31617
+ }
31618
+ revertOverride() {
31619
+ this.userOverride.override();
31572
31620
  }
31573
31621
  static ɵfac = i0.ɵɵngDeclareFactory({
31574
31622
  minVersion: "12.0.0",
31575
31623
  version: "22.2.1",
31576
31624
  ngImport: i0,
31577
- type: UserProfileDialog,
31625
+ type: SettingsAccountPanelComponent,
31578
31626
  deps: [],
31579
31627
  target: i0.ɵɵFactoryTarget.Component
31580
31628
  });
31581
31629
  static ɵcmp = i0.ɵɵngDeclareComponent({
31582
- minVersion: "17.2.0",
31630
+ minVersion: "17.0.0",
31583
31631
  version: "22.2.1",
31584
- type: UserProfileDialog,
31632
+ type: SettingsAccountPanelComponent,
31585
31633
  isStandalone: true,
31586
- selector: "user-profile-dialog, userprofiledialog, UserProfileDialog",
31587
- providers: [provideTranslocoScope("user-profile")],
31588
- viewQueries: [{
31589
- propertyName: "dialog",
31590
- first: true,
31591
- predicate: DialogComponent,
31592
- descendants: true,
31593
- isSignal: true
31594
- }],
31634
+ selector: "settings-account-panel",
31635
+ providers: [provideTranslocoScope("settings")],
31595
31636
  ngImport: i0,
31596
31637
  template: `
31597
- <div dialog #dialog="dialog" (closed)="call.end($event)">
31598
- <DialogContent size="md">
31599
- <DialogHeader [closeLabel]="'userProfile.close' | transloco">
31600
- <DialogTitle>{{ 'userProfile.title' | transloco }}</DialogTitle>
31601
- </DialogHeader>
31638
+ <h2 class="text-(length:--size-2xl) font-semibold">{{ "settings.account.title" | transloco }}</h2>
31639
+ <p class="text-(--font-primary-muted)">{{ "settings.account.description" | transloco }}</p>
31640
+
31641
+ @if (profileEnabled()) {
31642
+ <user-profile-form class="mt-(--spacing-md) min-h-0 flex-1" />
31643
+ } @else if (allowChangePassword()) {
31644
+ <!-- No profile to show: the password is all the account offers, as it was in the user menu. -->
31645
+ <change-password
31646
+ class="mt-(--spacing-md)"
31647
+ [surface]="false"
31648
+ [username]="username()"
31649
+ [redirectAfterSuccess]="false"
31650
+ [redirectAfterCancel]="false" />
31651
+ }
31602
31652
 
31603
- <DialogBody class="flex-1">
31604
- <user-profile-form class="flex flex-1 flex-col min-h-0" />
31605
- </DialogBody>
31606
- </DialogContent>
31653
+ <!-- Account-wide actions, one row each: what it does on the left, the action on the right. -->
31654
+ <div class="mt-(--spacing-md) flex flex-col border-t border-(--stroke-primary-light)">
31655
+ @if (principalStore.isOverridingUser()) {
31656
+ <div class="flex items-center justify-between gap-(--spacing-md) border-b border-(--stroke-primary-light) py-(--spacing-md)">
31657
+ <div>
31658
+ <p id="settings-revert-override-title" class="font-semibold">{{ "settings.account.revertOverride.title" | transloco }}</p>
31659
+ <p id="settings-revert-override-description" class="text-(--font-primary-muted)">{{ "settings.account.revertOverride.description" | transloco }}</p>
31660
+ </div>
31661
+ <button type="button" variant="secondary" class="shrink-0" aria-describedby="settings-revert-override-description" (click)="revertOverride()">
31662
+ {{ "settings.account.revertOverride.action" | transloco }}
31663
+ </button>
31664
+ </div>
31665
+ } @else if (principalStore.allowUserOverride()) {
31666
+ <div class="flex items-center justify-between gap-(--spacing-md) border-b border-(--stroke-primary-light) py-(--spacing-md)">
31667
+ <div>
31668
+ <p id="settings-override-title" class="font-semibold">{{ "settings.account.overrideUser.title" | transloco }}</p>
31669
+ <p id="settings-override-description" class="text-(--font-primary-muted)">{{ "settings.account.overrideUser.description" | transloco }}</p>
31670
+ </div>
31671
+ <button type="button" variant="secondary" class="shrink-0" aria-describedby="settings-override-description" (click)="overrideUser()">
31672
+ {{ "settings.account.overrideUser.action" | transloco }}
31673
+ </button>
31674
+ </div>
31675
+ }
31676
+
31677
+ <div class="flex items-center justify-between gap-(--spacing-md) py-(--spacing-md)">
31678
+ <div>
31679
+ <p id="settings-clear-data-title" class="font-semibold">{{ "settings.account.clearData.title" | transloco }}</p>
31680
+ <p id="settings-clear-data-description" class="text-(--font-primary-muted)">{{ "settings.account.clearData.description" | transloco }}</p>
31681
+ </div>
31682
+ <!-- Destructive: the secondary variant re-coloured with the error tokens. Every state of the
31683
+ variant (hover, active, focus) sets its own colours, so each one is overridden here — a
31684
+ plain border/text colour would only win while the button is at rest. -->
31685
+ <button
31686
+ type="button"
31687
+ variant="secondary"
31688
+ class="shrink-0 border-(--stroke-error-base) text-(--font-error-base) enabled:hover:bg-(--bg-error-lightest) enabled:hover:text-(--font-error-base) enabled:active:border-(--stroke-error-base) enabled:active:bg-(--bg-error-lightest) enabled:active:text-(--font-error-base) enabled:active:shadow-[inset_0_0_0_1px_var(--stroke-error-base)] enabled:focus-visible:border-(--stroke-error-base) enabled:focus-visible:text-(--font-error-base)"
31689
+ aria-describedby="settings-clear-data-description"
31690
+ (click)="resetSettings()">
31691
+ {{ "settings.account.clearData.action" | transloco }}
31692
+ </button>
31693
+ </div>
31607
31694
  </div>
31608
31695
  `,
31609
31696
  isInline: true,
31610
31697
  dependencies: [
31611
31698
  {
31612
- kind: "component",
31613
- type: DialogComponent,
31614
- selector: "dialog, [dialog]",
31615
- inputs: ["closeLabel"],
31616
- outputs: ["closed"],
31617
- exportAs: ["dialog"]
31618
- },
31619
- {
31620
- kind: "component",
31621
- type: DialogHeaderComponent,
31622
- selector: "DialogHeader",
31623
- inputs: ["closeLabel", "class"]
31699
+ kind: "directive",
31700
+ type: ButtonComponent,
31701
+ selector: "button",
31702
+ inputs: [
31703
+ "class",
31704
+ "variant",
31705
+ "scheme",
31706
+ "iconOnly",
31707
+ "size"
31708
+ ]
31709
+ },
31710
+ {
31711
+ kind: "component",
31712
+ type: UserProfileFormComponent,
31713
+ selector: "user-profile-form, UserProfileForm, userprofileform"
31714
+ },
31715
+ {
31716
+ kind: "component",
31717
+ type: ChangePasswordComponent,
31718
+ selector: "change-password, ChangePassword, changepassword",
31719
+ inputs: [
31720
+ "username",
31721
+ "alert",
31722
+ "redirectAfterSuccess",
31723
+ "redirectAfterCancel",
31724
+ "surface",
31725
+ "passwords"
31726
+ ],
31727
+ outputs: [
31728
+ "success",
31729
+ "cancel",
31730
+ "passwordsChange"
31731
+ ]
31732
+ },
31733
+ {
31734
+ kind: "pipe",
31735
+ type: TranslocoPipe,
31736
+ name: "transloco"
31737
+ }
31738
+ ]
31739
+ });
31740
+ };
31741
+ i0.ɵɵngDeclareClassMetadata({
31742
+ minVersion: "12.0.0",
31743
+ version: "22.2.1",
31744
+ ngImport: i0,
31745
+ type: SettingsAccountPanelComponent,
31746
+ decorators: [{
31747
+ type: Component,
31748
+ args: [{
31749
+ selector: "settings-account-panel",
31750
+ imports: [
31751
+ TranslocoPipe,
31752
+ ButtonComponent,
31753
+ UserProfileFormComponent,
31754
+ ChangePasswordComponent
31755
+ ],
31756
+ providers: [provideTranslocoScope("settings")],
31757
+ template: `
31758
+ <h2 class="text-(length:--size-2xl) font-semibold">{{ "settings.account.title" | transloco }}</h2>
31759
+ <p class="text-(--font-primary-muted)">{{ "settings.account.description" | transloco }}</p>
31760
+
31761
+ @if (profileEnabled()) {
31762
+ <user-profile-form class="mt-(--spacing-md) min-h-0 flex-1" />
31763
+ } @else if (allowChangePassword()) {
31764
+ <!-- No profile to show: the password is all the account offers, as it was in the user menu. -->
31765
+ <change-password
31766
+ class="mt-(--spacing-md)"
31767
+ [surface]="false"
31768
+ [username]="username()"
31769
+ [redirectAfterSuccess]="false"
31770
+ [redirectAfterCancel]="false" />
31771
+ }
31772
+
31773
+ <!-- Account-wide actions, one row each: what it does on the left, the action on the right. -->
31774
+ <div class="mt-(--spacing-md) flex flex-col border-t border-(--stroke-primary-light)">
31775
+ @if (principalStore.isOverridingUser()) {
31776
+ <div class="flex items-center justify-between gap-(--spacing-md) border-b border-(--stroke-primary-light) py-(--spacing-md)">
31777
+ <div>
31778
+ <p id="settings-revert-override-title" class="font-semibold">{{ "settings.account.revertOverride.title" | transloco }}</p>
31779
+ <p id="settings-revert-override-description" class="text-(--font-primary-muted)">{{ "settings.account.revertOverride.description" | transloco }}</p>
31780
+ </div>
31781
+ <button type="button" variant="secondary" class="shrink-0" aria-describedby="settings-revert-override-description" (click)="revertOverride()">
31782
+ {{ "settings.account.revertOverride.action" | transloco }}
31783
+ </button>
31784
+ </div>
31785
+ } @else if (principalStore.allowUserOverride()) {
31786
+ <div class="flex items-center justify-between gap-(--spacing-md) border-b border-(--stroke-primary-light) py-(--spacing-md)">
31787
+ <div>
31788
+ <p id="settings-override-title" class="font-semibold">{{ "settings.account.overrideUser.title" | transloco }}</p>
31789
+ <p id="settings-override-description" class="text-(--font-primary-muted)">{{ "settings.account.overrideUser.description" | transloco }}</p>
31790
+ </div>
31791
+ <button type="button" variant="secondary" class="shrink-0" aria-describedby="settings-override-description" (click)="overrideUser()">
31792
+ {{ "settings.account.overrideUser.action" | transloco }}
31793
+ </button>
31794
+ </div>
31795
+ }
31796
+
31797
+ <div class="flex items-center justify-between gap-(--spacing-md) py-(--spacing-md)">
31798
+ <div>
31799
+ <p id="settings-clear-data-title" class="font-semibold">{{ "settings.account.clearData.title" | transloco }}</p>
31800
+ <p id="settings-clear-data-description" class="text-(--font-primary-muted)">{{ "settings.account.clearData.description" | transloco }}</p>
31801
+ </div>
31802
+ <!-- Destructive: the secondary variant re-coloured with the error tokens. Every state of the
31803
+ variant (hover, active, focus) sets its own colours, so each one is overridden here — a
31804
+ plain border/text colour would only win while the button is at rest. -->
31805
+ <button
31806
+ type="button"
31807
+ variant="secondary"
31808
+ class="shrink-0 border-(--stroke-error-base) text-(--font-error-base) enabled:hover:bg-(--bg-error-lightest) enabled:hover:text-(--font-error-base) enabled:active:border-(--stroke-error-base) enabled:active:bg-(--bg-error-lightest) enabled:active:text-(--font-error-base) enabled:active:shadow-[inset_0_0_0_1px_var(--stroke-error-base)] enabled:focus-visible:border-(--stroke-error-base) enabled:focus-visible:text-(--font-error-base)"
31809
+ aria-describedby="settings-clear-data-description"
31810
+ (click)="resetSettings()">
31811
+ {{ "settings.account.clearData.action" | transloco }}
31812
+ </button>
31813
+ </div>
31814
+ </div>
31815
+ `
31816
+ }]
31817
+ }]
31818
+ });
31819
+ const THEMES$1 = [
31820
+ "light",
31821
+ "dark",
31822
+ "system"
31823
+ ];
31824
+ const LANGUAGES = [
31825
+ "en",
31826
+ "fr",
31827
+ "de"
31828
+ ];
31829
+ const isTheme = (value) => THEMES$1.includes(value);
31830
+ const isSupportedLanguage = (value) => LANGUAGES.includes(value);
31831
+ var SettingsAppearancePanelComponent = class SettingsAppearancePanelComponent {
31832
+ userSettingsStore = inject(UserSettingsStore);
31833
+ transloco = inject(TranslocoService);
31834
+ document = inject(DOCUMENT);
31835
+ lang = toSignal(this.transloco.langChanges$, { initialValue: this.transloco.getActiveLang() });
31836
+ loadedTranslations = toSignal(this.transloco.events$.pipe(filter((event) => event.type === "translationLoadSuccess"), scan((count) => count + 1, 0)), { initialValue: 0 });
31837
+ themeCodes = [...THEMES$1];
31838
+ languageCodes = [...LANGUAGES];
31839
+ themeIcons = {
31840
+ light: SunBrightIcon,
31841
+ dark: MoonIcon,
31842
+ system: DesktopIcon
31843
+ };
31844
+ languageIcons = {
31845
+ en: FlagEnglishIcon,
31846
+ fr: FlagFrenchIcon,
31847
+ de: FlagGermanIcon
31848
+ };
31849
+ languageLabels = {
31850
+ en: "English",
31851
+ fr: "Français",
31852
+ de: "Deutsch"
31853
+ };
31854
+ currentTheme = computed(() => this.userSettingsStore.userTheme() ?? "system", ...ngDevMode ? [{ debugName: "currentTheme" }] : /* istanbul ignore next */ []);
31855
+ currentLanguage = computed(() => this.lang(), ...ngDevMode ? [{ debugName: "currentLanguage" }] : /* istanbul ignore next */ []);
31856
+ themeLabel = (code) => {
31857
+ this.lang();
31858
+ this.loadedTranslations();
31859
+ return isTheme(code) ? this.transloco.translate(`settings.appearance.${code}Mode`) : "";
31860
+ };
31861
+ languageLabel = (code) => isSupportedLanguage(code) ? this.languageLabels[code] : code ?? "";
31862
+ themeIcon(code) {
31863
+ return isTheme(code) ? this.themeIcons[code] : null;
31864
+ }
31865
+ languageIcon(code) {
31866
+ return isSupportedLanguage(code) ? this.languageIcons[code] : null;
31867
+ }
31868
+ async onThemeChange(code) {
31869
+ if (!isTheme(code)) return;
31870
+ try {
31871
+ await this.userSettingsStore.setUserTheme(code);
31872
+ } catch (err) {
31873
+ error("set user theme failed", err);
31874
+ return;
31875
+ }
31876
+ const dark = code === "dark" || code === "system" && window.matchMedia?.("(prefers-color-scheme: dark)").matches;
31877
+ this.document.documentElement.classList.toggle("dark", dark);
31878
+ }
31879
+ onLanguageChange(code) {
31880
+ if (!isSupportedLanguage(code)) return;
31881
+ this.userSettingsStore.updateLanguage(code).catch((err) => error("update language failed", err));
31882
+ if (this.transloco.getActiveLang() !== code) this.transloco.setActiveLang(code);
31883
+ }
31884
+ static ɵfac = i0.ɵɵngDeclareFactory({
31885
+ minVersion: "12.0.0",
31886
+ version: "22.2.1",
31887
+ ngImport: i0,
31888
+ type: SettingsAppearancePanelComponent,
31889
+ deps: [],
31890
+ target: i0.ɵɵFactoryTarget.Component
31891
+ });
31892
+ static ɵcmp = i0.ɵɵngDeclareComponent({
31893
+ minVersion: "14.0.0",
31894
+ version: "22.2.1",
31895
+ type: SettingsAppearancePanelComponent,
31896
+ isStandalone: true,
31897
+ selector: "settings-appearance-panel",
31898
+ providers: [provideTranslocoScope("settings")],
31899
+ ngImport: i0,
31900
+ template: `
31901
+ <h2 class="text-(length:--size-2xl) font-semibold">{{ "settings.appearance.title" | transloco }}</h2>
31902
+ <p class="text-(--font-primary-muted)">{{ "settings.appearance.description" | transloco }}</p>
31903
+
31904
+ <div class="mt-(--spacing-lg) flex items-start justify-between gap-(--spacing-md)">
31905
+ <div>
31906
+ <label id="settings-theme-label" class="font-semibold" for="settings-theme">{{ "settings.appearance.theme" | transloco }}</label>
31907
+ <p id="settings-theme-description" class="text-(--font-primary-muted)">{{ "settings.appearance.themeDescription" | transloco }}</p>
31908
+ </div>
31909
+ <SelectMenu class="w-fit shrink-0" size="md" [options]="themeCodes" [label]="themeLabel" [value]="currentTheme()" (valueChange)="onThemeChange($event)">
31910
+ <button selectTrigger type="button" id="settings-theme" class="select-trigger" aria-describedby="settings-theme-description">
31911
+ <ng-container *ngComponentOutlet="themeIcon(currentTheme())" />
31912
+ <span class="select-trigger-label">{{ themeLabel(currentTheme()) }}</span>
31913
+ <chevron-down-icon class="select-chevron" />
31914
+ </button>
31915
+ <!-- The default option rendering is the label alone: the icon needs its own template. -->
31916
+ <ng-template #selectOption let-option>
31917
+ <span class="flex items-center gap-(--spacing-xs)">
31918
+ <ng-container *ngComponentOutlet="themeIcon(option)" />
31919
+ {{ themeLabel(option) }}
31920
+ </span>
31921
+ </ng-template>
31922
+ </SelectMenu>
31923
+ </div>
31924
+
31925
+ <hr class="my-(--spacing-md) border-(--stroke-primary-light)" />
31926
+
31927
+ <div class="flex items-start justify-between gap-(--spacing-md)">
31928
+ <div>
31929
+ <label id="settings-language-label" class="font-semibold" for="settings-language">{{ "settings.appearance.language" | transloco }}</label>
31930
+ <p id="settings-language-description" class="text-(--font-primary-muted)">{{ "settings.appearance.languageDescription" | transloco }}</p>
31931
+ </div>
31932
+ <SelectMenu
31933
+ class="w-fit shrink-0"
31934
+ size="md"
31935
+ [options]="languageCodes"
31936
+ [label]="languageLabel"
31937
+ [value]="currentLanguage()"
31938
+ (valueChange)="onLanguageChange($event)">
31939
+ <button selectTrigger type="button" id="settings-language" class="select-trigger" aria-describedby="settings-language-description">
31940
+ <ng-container *ngComponentOutlet="languageIcon(currentLanguage())" />
31941
+ <span class="select-trigger-label">{{ languageLabel(currentLanguage()) }}</span>
31942
+ <chevron-down-icon class="select-chevron" />
31943
+ </button>
31944
+ <ng-template #selectOption let-option>
31945
+ <span class="flex items-center gap-(--spacing-xs)">
31946
+ <ng-container *ngComponentOutlet="languageIcon(option)" />
31947
+ {{ languageLabel(option) }}
31948
+ </span>
31949
+ </ng-template>
31950
+ </SelectMenu>
31951
+ </div>
31952
+ `,
31953
+ isInline: true,
31954
+ dependencies: [
31955
+ {
31956
+ kind: "directive",
31957
+ type: NgComponentOutlet,
31958
+ selector: "[ngComponentOutlet]",
31959
+ inputs: [
31960
+ "ngComponentOutlet",
31961
+ "ngComponentOutletInputs",
31962
+ "ngComponentOutletInjector",
31963
+ "ngComponentOutletEnvironmentInjector",
31964
+ "ngComponentOutletContent",
31965
+ "ngComponentOutletNgModule"
31966
+ ],
31967
+ exportAs: ["ngComponentOutlet"]
31968
+ },
31969
+ {
31970
+ kind: "component",
31971
+ type: SelectComponent,
31972
+ selector: "SelectMenu,select-menu",
31973
+ inputs: [
31974
+ "value",
31975
+ "options",
31976
+ "placement",
31977
+ "size",
31978
+ "fieldLabel",
31979
+ "aria-label",
31980
+ "placeholder",
31981
+ "hint",
31982
+ "disabled",
31983
+ "class",
31984
+ "selectionMode",
31985
+ "multiple",
31986
+ "wrap",
31987
+ "key",
31988
+ "label",
31989
+ "optionDisabled",
31990
+ "noOptionsText",
31991
+ "contentClass"
31992
+ ],
31993
+ outputs: ["valueChange", "touch"],
31994
+ exportAs: ["select"]
31995
+ },
31996
+ {
31997
+ kind: "directive",
31998
+ type: SelectTriggerDirective,
31999
+ selector: "[selectTrigger]",
32000
+ exportAs: ["selectTrigger"]
32001
+ },
32002
+ {
32003
+ kind: "component",
32004
+ type: ChevronDownIcon,
32005
+ selector: "chevron-down, chevron-down-icon, ChevronDown, ChevronDownIcon",
32006
+ inputs: ["class"]
32007
+ },
32008
+ {
32009
+ kind: "pipe",
32010
+ type: TranslocoPipe,
32011
+ name: "transloco"
32012
+ }
32013
+ ]
32014
+ });
32015
+ };
32016
+ i0.ɵɵngDeclareClassMetadata({
32017
+ minVersion: "12.0.0",
32018
+ version: "22.2.1",
32019
+ ngImport: i0,
32020
+ type: SettingsAppearancePanelComponent,
32021
+ decorators: [{
32022
+ type: Component,
32023
+ args: [{
32024
+ selector: "settings-appearance-panel",
32025
+ imports: [
32026
+ NgComponentOutlet,
32027
+ TranslocoPipe,
32028
+ SelectComponent,
32029
+ SelectTriggerDirective,
32030
+ ChevronDownIcon
32031
+ ],
32032
+ providers: [provideTranslocoScope("settings")],
32033
+ template: `
32034
+ <h2 class="text-(length:--size-2xl) font-semibold">{{ "settings.appearance.title" | transloco }}</h2>
32035
+ <p class="text-(--font-primary-muted)">{{ "settings.appearance.description" | transloco }}</p>
32036
+
32037
+ <div class="mt-(--spacing-lg) flex items-start justify-between gap-(--spacing-md)">
32038
+ <div>
32039
+ <label id="settings-theme-label" class="font-semibold" for="settings-theme">{{ "settings.appearance.theme" | transloco }}</label>
32040
+ <p id="settings-theme-description" class="text-(--font-primary-muted)">{{ "settings.appearance.themeDescription" | transloco }}</p>
32041
+ </div>
32042
+ <SelectMenu class="w-fit shrink-0" size="md" [options]="themeCodes" [label]="themeLabel" [value]="currentTheme()" (valueChange)="onThemeChange($event)">
32043
+ <button selectTrigger type="button" id="settings-theme" class="select-trigger" aria-describedby="settings-theme-description">
32044
+ <ng-container *ngComponentOutlet="themeIcon(currentTheme())" />
32045
+ <span class="select-trigger-label">{{ themeLabel(currentTheme()) }}</span>
32046
+ <chevron-down-icon class="select-chevron" />
32047
+ </button>
32048
+ <!-- The default option rendering is the label alone: the icon needs its own template. -->
32049
+ <ng-template #selectOption let-option>
32050
+ <span class="flex items-center gap-(--spacing-xs)">
32051
+ <ng-container *ngComponentOutlet="themeIcon(option)" />
32052
+ {{ themeLabel(option) }}
32053
+ </span>
32054
+ </ng-template>
32055
+ </SelectMenu>
32056
+ </div>
32057
+
32058
+ <hr class="my-(--spacing-md) border-(--stroke-primary-light)" />
32059
+
32060
+ <div class="flex items-start justify-between gap-(--spacing-md)">
32061
+ <div>
32062
+ <label id="settings-language-label" class="font-semibold" for="settings-language">{{ "settings.appearance.language" | transloco }}</label>
32063
+ <p id="settings-language-description" class="text-(--font-primary-muted)">{{ "settings.appearance.languageDescription" | transloco }}</p>
32064
+ </div>
32065
+ <SelectMenu
32066
+ class="w-fit shrink-0"
32067
+ size="md"
32068
+ [options]="languageCodes"
32069
+ [label]="languageLabel"
32070
+ [value]="currentLanguage()"
32071
+ (valueChange)="onLanguageChange($event)">
32072
+ <button selectTrigger type="button" id="settings-language" class="select-trigger" aria-describedby="settings-language-description">
32073
+ <ng-container *ngComponentOutlet="languageIcon(currentLanguage())" />
32074
+ <span class="select-trigger-label">{{ languageLabel(currentLanguage()) }}</span>
32075
+ <chevron-down-icon class="select-chevron" />
32076
+ </button>
32077
+ <ng-template #selectOption let-option>
32078
+ <span class="flex items-center gap-(--spacing-xs)">
32079
+ <ng-container *ngComponentOutlet="languageIcon(option)" />
32080
+ {{ languageLabel(option) }}
32081
+ </span>
32082
+ </ng-template>
32083
+ </SelectMenu>
32084
+ </div>
32085
+ `
32086
+ }]
32087
+ }]
32088
+ });
32089
+ var SettingsShortcutsPanelComponent = class SettingsShortcutsPanelComponent {
32090
+ static ɵfac = i0.ɵɵngDeclareFactory({
32091
+ minVersion: "12.0.0",
32092
+ version: "22.2.1",
32093
+ ngImport: i0,
32094
+ type: SettingsShortcutsPanelComponent,
32095
+ deps: [],
32096
+ target: i0.ɵɵFactoryTarget.Component
32097
+ });
32098
+ static ɵcmp = i0.ɵɵngDeclareComponent({
32099
+ minVersion: "14.0.0",
32100
+ version: "22.2.1",
32101
+ type: SettingsShortcutsPanelComponent,
32102
+ isStandalone: true,
32103
+ selector: "settings-shortcuts-panel",
32104
+ providers: [provideTranslocoScope("settings")],
32105
+ ngImport: i0,
32106
+ template: `
32107
+ <h2 class="text-(length:--size-2xl) font-semibold">{{ "settings.shortcuts.title" | transloco }}</h2>
32108
+ <p class="mb-(--spacing-md) text-(--font-primary-muted)">{{ "settings.shortcuts.description" | transloco }}</p>
32109
+ <keyboard-shortcuts-list />
32110
+ `,
32111
+ isInline: true,
32112
+ dependencies: [{
32113
+ kind: "component",
32114
+ type: KeyboardShortcutsListComponent,
32115
+ selector: "keyboard-shortcuts-list"
32116
+ }, {
32117
+ kind: "pipe",
32118
+ type: TranslocoPipe,
32119
+ name: "transloco"
32120
+ }]
32121
+ });
32122
+ };
32123
+ i0.ɵɵngDeclareClassMetadata({
32124
+ minVersion: "12.0.0",
32125
+ version: "22.2.1",
32126
+ ngImport: i0,
32127
+ type: SettingsShortcutsPanelComponent,
32128
+ decorators: [{
32129
+ type: Component,
32130
+ args: [{
32131
+ selector: "settings-shortcuts-panel",
32132
+ imports: [TranslocoPipe, KeyboardShortcutsListComponent],
32133
+ providers: [provideTranslocoScope("settings")],
32134
+ template: `
32135
+ <h2 class="text-(length:--size-2xl) font-semibold">{{ "settings.shortcuts.title" | transloco }}</h2>
32136
+ <p class="mb-(--spacing-md) text-(--font-primary-muted)">{{ "settings.shortcuts.description" | transloco }}</p>
32137
+ <keyboard-shortcuts-list />
32138
+ `
32139
+ }]
32140
+ }]
32141
+ });
32142
+ let active;
32143
+ var SettingsDialogComponent = class SettingsDialogComponent {
32144
+ call = injectCallRef();
32145
+ dialog = viewChild(DialogComponent, ...ngDevMode ? [{ debugName: "dialog" }] : /* istanbul ignore next */ []);
32146
+ items = [
32147
+ {
32148
+ id: "account",
32149
+ labelKey: "settings.account.nav"
32150
+ },
32151
+ {
32152
+ id: "appearance",
32153
+ labelKey: "settings.appearance.nav"
32154
+ },
32155
+ {
32156
+ id: "shortcuts",
32157
+ labelKey: "settings.shortcuts.nav"
32158
+ }
32159
+ ];
32160
+ panel = signal(this.call.props()?.panel ?? "account", ...ngDevMode ? [{ debugName: "panel" }] : /* istanbul ignore next */ []);
32161
+ duplicate = active !== void 0;
32162
+ constructor() {
32163
+ if (active) {
32164
+ active.panel.set(this.panel());
32165
+ afterNextRender(() => this.call.end("dialog-close"));
32166
+ return;
32167
+ }
32168
+ active = this;
32169
+ inject(DestroyRef).onDestroy(() => {
32170
+ if (active === this) active = void 0;
32171
+ });
32172
+ afterNextRender(() => this.dialog()?.open());
32173
+ }
32174
+ static ɵfac = i0.ɵɵngDeclareFactory({
32175
+ minVersion: "12.0.0",
32176
+ version: "22.2.1",
32177
+ ngImport: i0,
32178
+ type: SettingsDialogComponent,
32179
+ deps: [],
32180
+ target: i0.ɵɵFactoryTarget.Component
32181
+ });
32182
+ static ɵcmp = i0.ɵɵngDeclareComponent({
32183
+ minVersion: "17.0.0",
32184
+ version: "22.2.1",
32185
+ type: SettingsDialogComponent,
32186
+ isStandalone: true,
32187
+ selector: "settings-dialog",
32188
+ providers: [provideTranslocoScope("settings")],
32189
+ viewQueries: [{
32190
+ propertyName: "dialog",
32191
+ first: true,
32192
+ predicate: DialogComponent,
32193
+ descendants: true,
32194
+ isSignal: true
32195
+ }],
32196
+ ngImport: i0,
32197
+ template: `
32198
+ @if (!duplicate) {
32199
+ <div dialog #dialog="dialog" [closeLabel]="'settings.close' | transloco" (closed)="call.end($event)">
32200
+ <DialogContent size="xl" class="h-[min(48rem,calc(100dvh-2rem))] flex-row! items-stretch!">
32201
+ <div class="flex w-64 shrink-0 flex-col gap-(--spacing-3xs) bg-(--layer-page-on-layer) p-(--spacing-sm)">
32202
+ <button
32203
+ type="button"
32204
+ variant="none"
32205
+ class="modal-close mb-(--spacing-sm)"
32206
+ [attr.aria-label]="'settings.close' | transloco"
32207
+ (click)="dialog.close()">
32208
+ <XMarkIcon aria-hidden="true" />
32209
+ </button>
32210
+
32211
+ <!-- Panels are a navigation, not a tablist: each one is a button, the current one is
32212
+ flagged with aria-current, which is what a screen reader reads out. -->
32213
+ <nav [attr.aria-label]="'settings.title' | transloco" class="flex flex-col gap-(--spacing-3xs)">
32214
+ @for (item of items; track item.id) {
32215
+ <button
32216
+ type="button"
32217
+ variant="none"
32218
+ class="flex h-(--row-height-md) w-full cursor-pointer items-center justify-start gap-(--spacing-xs) rounded-(--radius-sm) border-0 bg-transparent px-(--spacing-xs) text-left text-(--layer-page-font) transition-colors hover:bg-(--bg-primary-light) focus-visible:outline-2 focus-visible:outline-offset-0 focus-visible:outline-(--stroke-focus) aria-[current=page]:bg-(--bg-primary-base) aria-[current=page]:text-(--font-neutral-white) aria-[current=page]:hover:bg-(--bg-primary-base)"
32219
+ [attr.aria-current]="panel() === item.id ? 'page' : null"
32220
+ (click)="panel.set(item.id)">
32221
+ @switch (item.id) {
32222
+ @case ("account") {
32223
+ <user-icon aria-hidden="true" />
32224
+ }
32225
+ @case ("appearance") {
32226
+ <palette-icon aria-hidden="true" />
32227
+ }
32228
+ @case ("shortcuts") {
32229
+ <keyboard-icon aria-hidden="true" />
32230
+ }
32231
+ }
32232
+ {{ item.labelKey | transloco }}
32233
+ </button>
32234
+ }
32235
+ </nav>
32236
+ </div>
32237
+
32238
+ <div class="m-(--spacing-xs) flex min-w-0 flex-1 flex-col overflow-y-auto rounded-(--radius-lg) bg-(--layer-page-layer) p-(--spacing-xl)">
32239
+ @switch (panel()) {
32240
+ @case ("account") {
32241
+ <settings-account-panel class="flex min-h-0 flex-1 flex-col" />
32242
+ }
32243
+ @case ("appearance") {
32244
+ <settings-appearance-panel />
32245
+ }
32246
+ @case ("shortcuts") {
32247
+ <settings-shortcuts-panel />
32248
+ }
32249
+ }
32250
+ </div>
32251
+ </DialogContent>
32252
+ </div>
32253
+ }
32254
+ `,
32255
+ isInline: true,
32256
+ dependencies: [
32257
+ {
32258
+ kind: "directive",
32259
+ type: ButtonComponent,
32260
+ selector: "button",
32261
+ inputs: [
32262
+ "class",
32263
+ "variant",
32264
+ "scheme",
32265
+ "iconOnly",
32266
+ "size"
32267
+ ]
32268
+ },
32269
+ {
32270
+ kind: "component",
32271
+ type: DialogComponent,
32272
+ selector: "dialog, [dialog]",
32273
+ inputs: ["closeLabel"],
32274
+ outputs: ["closed"],
32275
+ exportAs: ["dialog"]
31624
32276
  },
31625
32277
  {
31626
32278
  kind: "directive",
@@ -31633,23 +32285,43 @@ var UserProfileDialog = class UserProfileDialog {
31633
32285
  ]
31634
32286
  },
31635
32287
  {
31636
- kind: "directive",
31637
- type: DialogBodyComponent,
31638
- selector: "DialogBody",
32288
+ kind: "component",
32289
+ type: UserIcon,
32290
+ selector: "user, user-icon, User, UserIcon",
31639
32291
  inputs: ["class"]
31640
32292
  },
31641
32293
  {
31642
32294
  kind: "component",
31643
- type: UserProfileFormComponent,
31644
- selector: "user-profile-form, UserProfileForm, userprofileform",
31645
- inputs: ["currentLanguage"],
31646
- outputs: ["currentLanguageChange"]
32295
+ type: PaletteIcon,
32296
+ selector: "palette, palette-icon, Palette, PaletteIcon",
32297
+ inputs: ["class"]
31647
32298
  },
31648
32299
  {
31649
- kind: "directive",
31650
- type: DialogTitleComponent,
31651
- selector: "DialogTitle",
31652
- inputs: ["id", "class"]
32300
+ kind: "component",
32301
+ type: KeyboardIcon,
32302
+ selector: "keyboard, keyboard-icon, Keyboard, KeyboardIcon",
32303
+ inputs: ["class"]
32304
+ },
32305
+ {
32306
+ kind: "component",
32307
+ type: XMarkIcon,
32308
+ selector: "x-mark, x-mark-icon, XMark, XMarkIcon",
32309
+ inputs: ["class"]
32310
+ },
32311
+ {
32312
+ kind: "component",
32313
+ type: SettingsAccountPanelComponent,
32314
+ selector: "settings-account-panel"
32315
+ },
32316
+ {
32317
+ kind: "component",
32318
+ type: SettingsAppearancePanelComponent,
32319
+ selector: "settings-appearance-panel"
32320
+ },
32321
+ {
32322
+ kind: "component",
32323
+ type: SettingsShortcutsPanelComponent,
32324
+ selector: "settings-shortcuts-panel"
31653
32325
  },
31654
32326
  {
31655
32327
  kind: "pipe",
@@ -31663,33 +32335,82 @@ i0.ɵɵngDeclareClassMetadata({
31663
32335
  minVersion: "12.0.0",
31664
32336
  version: "22.2.1",
31665
32337
  ngImport: i0,
31666
- type: UserProfileDialog,
32338
+ type: SettingsDialogComponent,
31667
32339
  decorators: [{
31668
32340
  type: Component,
31669
32341
  args: [{
31670
- selector: "user-profile-dialog, userprofiledialog, UserProfileDialog",
32342
+ selector: "settings-dialog",
31671
32343
  imports: [
32344
+ TranslocoPipe,
32345
+ ButtonComponent,
31672
32346
  DialogComponent,
31673
- DialogHeaderComponent,
31674
32347
  DialogContentComponent,
31675
- DialogBodyComponent,
31676
- UserProfileFormComponent,
31677
- DialogTitleComponent,
31678
- TranslocoPipe
32348
+ UserIcon,
32349
+ PaletteIcon,
32350
+ KeyboardIcon,
32351
+ XMarkIcon,
32352
+ SettingsAccountPanelComponent,
32353
+ SettingsAppearancePanelComponent,
32354
+ SettingsShortcutsPanelComponent
31679
32355
  ],
31680
- providers: [provideTranslocoScope("user-profile")],
32356
+ providers: [provideTranslocoScope("settings")],
31681
32357
  template: `
31682
- <div dialog #dialog="dialog" (closed)="call.end($event)">
31683
- <DialogContent size="md">
31684
- <DialogHeader [closeLabel]="'userProfile.close' | transloco">
31685
- <DialogTitle>{{ 'userProfile.title' | transloco }}</DialogTitle>
31686
- </DialogHeader>
32358
+ @if (!duplicate) {
32359
+ <div dialog #dialog="dialog" [closeLabel]="'settings.close' | transloco" (closed)="call.end($event)">
32360
+ <DialogContent size="xl" class="h-[min(48rem,calc(100dvh-2rem))] flex-row! items-stretch!">
32361
+ <div class="flex w-64 shrink-0 flex-col gap-(--spacing-3xs) bg-(--layer-page-on-layer) p-(--spacing-sm)">
32362
+ <button
32363
+ type="button"
32364
+ variant="none"
32365
+ class="modal-close mb-(--spacing-sm)"
32366
+ [attr.aria-label]="'settings.close' | transloco"
32367
+ (click)="dialog.close()">
32368
+ <XMarkIcon aria-hidden="true" />
32369
+ </button>
31687
32370
 
31688
- <DialogBody class="flex-1">
31689
- <user-profile-form class="flex flex-1 flex-col min-h-0" />
31690
- </DialogBody>
31691
- </DialogContent>
31692
- </div>
32371
+ <!-- Panels are a navigation, not a tablist: each one is a button, the current one is
32372
+ flagged with aria-current, which is what a screen reader reads out. -->
32373
+ <nav [attr.aria-label]="'settings.title' | transloco" class="flex flex-col gap-(--spacing-3xs)">
32374
+ @for (item of items; track item.id) {
32375
+ <button
32376
+ type="button"
32377
+ variant="none"
32378
+ class="flex h-(--row-height-md) w-full cursor-pointer items-center justify-start gap-(--spacing-xs) rounded-(--radius-sm) border-0 bg-transparent px-(--spacing-xs) text-left text-(--layer-page-font) transition-colors hover:bg-(--bg-primary-light) focus-visible:outline-2 focus-visible:outline-offset-0 focus-visible:outline-(--stroke-focus) aria-[current=page]:bg-(--bg-primary-base) aria-[current=page]:text-(--font-neutral-white) aria-[current=page]:hover:bg-(--bg-primary-base)"
32379
+ [attr.aria-current]="panel() === item.id ? 'page' : null"
32380
+ (click)="panel.set(item.id)">
32381
+ @switch (item.id) {
32382
+ @case ("account") {
32383
+ <user-icon aria-hidden="true" />
32384
+ }
32385
+ @case ("appearance") {
32386
+ <palette-icon aria-hidden="true" />
32387
+ }
32388
+ @case ("shortcuts") {
32389
+ <keyboard-icon aria-hidden="true" />
32390
+ }
32391
+ }
32392
+ {{ item.labelKey | transloco }}
32393
+ </button>
32394
+ }
32395
+ </nav>
32396
+ </div>
32397
+
32398
+ <div class="m-(--spacing-xs) flex min-w-0 flex-1 flex-col overflow-y-auto rounded-(--radius-lg) bg-(--layer-page-layer) p-(--spacing-xl)">
32399
+ @switch (panel()) {
32400
+ @case ("account") {
32401
+ <settings-account-panel class="flex min-h-0 flex-1 flex-col" />
32402
+ }
32403
+ @case ("appearance") {
32404
+ <settings-appearance-panel />
32405
+ }
32406
+ @case ("shortcuts") {
32407
+ <settings-shortcuts-panel />
32408
+ }
32409
+ }
32410
+ </div>
32411
+ </DialogContent>
32412
+ </div>
32413
+ }
31693
32414
  `
31694
32415
  }]
31695
32416
  }],
@@ -31699,7 +32420,7 @@ i0.ɵɵngDeclareClassMetadata({
31699
32420
  args: [i0.forwardRef(() => DialogComponent), { isSignal: true }]
31700
32421
  }] }
31701
32422
  });
31702
- const UserProfile = createCallable(UserProfileDialog);
32423
+ const Settings = createCallable(SettingsDialogComponent);
31703
32424
  const auditInterceptorFn = (request, next) => {
31704
32425
  if (!request.url.includes("api/v1")) return next(request);
31705
32426
  if (isJsonable(request.body) && !(request.body instanceof HttpParams)) addAuditAdditionalInfo(request.body);
@@ -32150,6 +32871,6 @@ function provideSessionExpiry(options = {}) {
32150
32871
  const queryNameResolver = () => {
32151
32872
  return inject(AppStore).getDefaultQuery()?.name || "";
32152
32873
  };
32153
- 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, AlertsComponent, AppService, AppStore, ApplicationService, ApplicationStore, AppliedFiltersComponent, ArticleEntities, ArticleEntitiesTabPanel, ArticleExtracts, ArticleExtractsTabPanel, ArticleHighlights, ArticleLabels, ArticleLabelsTabPanel, ArticleMatchLocations, ArticleMatchLocationsTabPanel, ArticleSimilarDocuments, ArticleSimilarDocumentsTabPanel, AsideFiltersComponent, AuditFeedbackType, AuditService, AuthGuard, AuthPageComponent, AutocompleteService, BOOKMARKS_CONFIG, BOOKMARKS_OPTIONS, BackdropComponent, BackdropService, BookmarkButtonComponent, BookmarksComponent, COLLECTIONS_CONFIG, COLLECTIONS_OPTIONS, COMPONENTS_FOR_DOCUMENT_TYPE, ChangePasswordComponent, ChildMarkerDirective, CollectionsComponent, CollectionsDialog, ConverterSelectComponent, DeleteCollection, DeleteCollectionDialog, DidYouMeanComponent, DocumentLocatorComponent, EditLabels, ErrorComponent, Export, ExportDialog, ExportService, FILTERS_BREAKPOINT, FILTER_DATE_ALLOW_CUSTOM_RANGE, FORM_ALERT_CLASS, FORM_BARE_CLASS, FORM_ERROR_CLASS, FORM_FIELD_CLASS, FORM_FOOTER_CLASS, FORM_LABEL_CLASS, FORM_SURFACE_CLASS, FaIconComponent, FeatureFlags, FeatureFlagsDialogComponent, Feedback, FeedbackDialogComponent, FileSizePipe, FilterButtonComponent, FiltersBarComponent, FloatingToolbarComponent, ForgotPasswordComponent, HIGHLIGHTS, HeatmapComponent, HighlightWordPipe, InfinityScrollDirective, InlineWorker, JsonMethodPluginService, KeyboardShortcutsDialog, KeyboardShortcutsDialogComponent, KeyboardShortcutsService, LabelService, LabelsEditDialog, ListPanelItemDirective, LoadingComponent, MarkdownPipe, MarkdownRendererService, MetadataComponent, MissingTermsComponent, MoreButtonComponent, MoreComponent, MultiSelectLabelsComponent, MultiSelectionToolbarComponent, NON_SEARCHABLE_COLUMNS, NON_SEARCHABLE_DEFAULTS, NavbarTabsComponent, NavigationService, NoResultComponent, OpenArticleOnCtrlEnterDirective, OperatorPipe, OverflowItemDirective, OverflowManagerDirective, OverflowStopDirective, OverrideUser, OverrideUserDialogComponent, PREVIEW_ASSISTANT_COMPONENT, PREVIEW_CONFIG, PagerComponent, PreviewActionsComponent, PreviewContentComponent, PreviewHeaderComponent, PreviewNavigator, PreviewService, PrincipalService, PrincipalStore, QueryParamsStore, QueryService, RECENT_SEARCHES_CONFIG, RECENT_SEARCHES_OPTIONS, ROUTE_COMPONENTS, RecentSearchesComponent, ResetUserSettings, ResetUserSettingsDialogComponent, SAVED_SEARCHES_CONFIG, SAVED_SEARCHES_OPTIONS, SavedSearch, SavedSearchDialog, SavedSearchPopover, SavedSearchesComponent, SavedSearchesService, SearchFeedbackComponent, SearchInputFooter, SearchService, SelectArticleDirective, SelectionService, SelectionStore, ShortcutDirective, ShowBookmarkDirective, SignInComponent, SignedOutComponent, SortSelectorComponent, SourceComponent, SourceIconPipe, SponsoredResultsComponent, SyslangPipe, THEMES, TagCloudComponent, TextChunkService, ThemeProviderDirective, ThemeSelectorComponent, ThemeStore, ThemeToggleComponent, TranslocoDateImpurePipe, UserOverrideService, UserProfile, UserProfileDialog, 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, injectAdvancedFilters, injectAtomicClient, injectRouteNavigation, injectUserSettingsPatch, isAuthRoute, isSignInInFlight, itemLabel, layoutWordCloud, legacyFilterToFilter, makeWeightScale, multiSelectionToolbarVariants, paletteColor, processCssVars, provideAtomicClient, provideAtomicNotifications, provideAtomicScope, provideDocumentLang, provideKeyboardShortcuts, provideSessionExpiry, quantileScale, queryNameResolver, rebuildWithout, seriesPalette, signIn, themeColorNameToCssVariable, themeColorsToCssVariables, timelinePalette, toAriaKeyshortcuts, toDisplayParts, toPercentages, toSeries, toastInterceptorFn, weekdaysFromFlags, withAggregationsFeatures, withAlertsFeatures, withAlpha, withAppFeatures, withApplicationFeatures, withAssistantFeatures, withBasketsFeatures, withBookmarkFeatures, withBootstrapApp, withExtractsFeatures, withFetch, withMultiSelectionFeatures, withQueryParamsFeatures, withRecentSearchesFeatures, withSavedSearchesFeatures, withSelectionFeatures, withThemeBodyHook, withThemes, withThemesFeatures, withUserSettingsFeatures };
32874
+ 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, AlertsComponent, AppService, AppStore, ApplicationService, ApplicationStore, AppliedFiltersComponent, ArticleEntities, ArticleEntitiesTabPanel, ArticleExtracts, ArticleExtractsTabPanel, ArticleHighlights, ArticleLabels, ArticleLabelsTabPanel, ArticleMatchLocations, ArticleMatchLocationsTabPanel, ArticleSimilarDocuments, ArticleSimilarDocumentsTabPanel, AsideFiltersComponent, AuditFeedbackType, AuditService, AuthGuard, AuthPageComponent, AutocompleteService, BOOKMARKS_CONFIG, BOOKMARKS_OPTIONS, BackdropComponent, BackdropService, BookmarkButtonComponent, BookmarksComponent, COLLECTIONS_CONFIG, COLLECTIONS_OPTIONS, COMPONENTS_FOR_DOCUMENT_TYPE, ChangePasswordComponent, ChildMarkerDirective, CollectionsComponent, CollectionsDialog, ConverterSelectComponent, DeleteCollection, DeleteCollectionDialog, DidYouMeanComponent, DocumentLocatorComponent, EditLabels, ErrorComponent, Export, ExportDialog, ExportService, FILTERS_BREAKPOINT, FILTER_DATE_ALLOW_CUSTOM_RANGE, FORM_ALERT_CLASS, FORM_BARE_CLASS, FORM_ERROR_CLASS, FORM_FIELD_CLASS, FORM_FOOTER_CLASS, FORM_LABEL_CLASS, FORM_SURFACE_CLASS, FaIconComponent, FeatureFlags, FeatureFlagsDialogComponent, Feedback, FeedbackDialogComponent, FileSizePipe, FilterButtonComponent, FiltersBarComponent, FloatingToolbarComponent, ForgotPasswordComponent, HIGHLIGHTS, HeatmapComponent, HighlightWordPipe, InfinityScrollDirective, InlineWorker, JsonMethodPluginService, KeyboardShortcutsDialog, KeyboardShortcutsDialogComponent, KeyboardShortcutsListComponent, KeyboardShortcutsService, LabelService, LabelsEditDialog, ListPanelItemDirective, LoadingComponent, MarkdownPipe, MarkdownRendererService, MetadataComponent, MissingTermsComponent, MoreButtonComponent, MoreComponent, MultiSelectLabelsComponent, MultiSelectionToolbarComponent, NON_SEARCHABLE_COLUMNS, NON_SEARCHABLE_DEFAULTS, NavbarTabsComponent, NavigationService, NoResultComponent, OpenArticleOnCtrlEnterDirective, OperatorPipe, OverflowItemDirective, OverflowManagerDirective, OverflowStopDirective, OverrideUser, OverrideUserDialogComponent, PREVIEW_ASSISTANT_COMPONENT, PREVIEW_CONFIG, PagerComponent, PreviewActionsComponent, PreviewContentComponent, PreviewHeaderComponent, PreviewNavigator, PreviewService, PrincipalService, PrincipalStore, QueryParamsStore, QueryService, RECENT_SEARCHES_CONFIG, RECENT_SEARCHES_OPTIONS, ROUTE_COMPONENTS, RecentSearchesComponent, ResetUserSettings, ResetUserSettingsDialogComponent, SAVED_SEARCHES_CONFIG, SAVED_SEARCHES_OPTIONS, SavedSearch, SavedSearchDialog, SavedSearchPopover, SavedSearchesComponent, SavedSearchesService, SearchFeedbackComponent, SearchInputFooter, SearchService, SelectArticleDirective, SelectionService, SelectionStore, Settings, SettingsAccountPanelComponent, 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, injectAdvancedFilters, injectAtomicClient, injectRouteNavigation, injectUserSettingsPatch, isAuthRoute, isSignInInFlight, itemLabel, layoutWordCloud, legacyFilterToFilter, makeWeightScale, multiSelectionToolbarVariants, paletteColor, processCssVars, provideAtomicClient, provideAtomicNotifications, provideAtomicScope, provideDocumentLang, provideKeyboardShortcuts, provideSessionExpiry, quantileScale, queryNameResolver, rebuildWithout, seriesPalette, signIn, themeColorNameToCssVariable, themeColorsToCssVariables, timelinePalette, toAriaKeyshortcuts, toDisplayParts, toPercentages, toSeries, toastInterceptorFn, weekdaysFromFlags, withAggregationsFeatures, withAlertsFeatures, withAlpha, withAppFeatures, withApplicationFeatures, withAssistantFeatures, withBasketsFeatures, withBookmarkFeatures, withBootstrapApp, withExtractsFeatures, withFetch, withMultiSelectionFeatures, withQueryParamsFeatures, withRecentSearchesFeatures, withSavedSearchesFeatures, withSelectionFeatures, withThemeBodyHook, withThemes, withThemesFeatures, withUserSettingsFeatures };
32154
32875
 
32155
32876
  //# sourceMappingURL=sinequa-atomic-angular.mjs.map