@sinequa/atomic-angular 2.13.0 → 2.14.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.
- package/features/advanced-search/i18n/de.json +9 -1
- package/features/advanced-search/i18n/en.json +9 -1
- package/features/advanced-search/i18n/fr.json +9 -1
- package/features/alerts/i18n/de.json +2 -1
- package/features/alerts/i18n/en.json +2 -1
- package/features/alerts/i18n/fr.json +2 -1
- package/features/quick-filters/i18n/de.json +2 -1
- package/features/quick-filters/i18n/en.json +2 -1
- package/features/quick-filters/i18n/fr.json +2 -1
- package/features/searches/i18n/de.json +2 -1
- package/features/searches/i18n/en.json +2 -1
- package/features/searches/i18n/fr.json +2 -1
- package/fesm2022/sinequa-atomic-angular-document-workspaces.mjs +45 -43
- package/fesm2022/sinequa-atomic-angular-document-workspaces.mjs.map +1 -1
- package/fesm2022/sinequa-atomic-angular.mjs +1251 -375
- package/fesm2022/sinequa-atomic-angular.mjs.map +1 -1
- package/package.json +1 -1
- package/types/sinequa-atomic-angular.d.ts +153 -68
|
@@ -2,7 +2,7 @@ import * as i0 from "@angular/core";
|
|
|
2
2
|
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, DOCUMENT, DestroyRef, Directive, ElementRef, EnvironmentInjector, HostBinding, HostListener, Inject, Injectable, InjectionToken, Injector, LOCALE_ID, Optional, Pipe, Service, TemplateRef, ViewContainerRef, ViewEncapsulation, afterNextRender, afterRenderEffect, assertInInjectionContext, booleanAttribute, computed, contentChild, contentChildren, effect, inject, input, isDevMode, linkedSignal, makeEnvironmentProviders, model, numberAttribute, output, provideEnvironmentInitializer, resource, runInInjectionContext, signal, untracked, viewChild, viewChildren } from "@angular/core";
|
|
3
3
|
import { BehaviorSubject, EMPTY, Subject, Subscription, catchError, debounceTime, filter, finalize, firstValueFrom, from, fromEvent, map, of, scan, shareReplay, switchMap, tap, throwError } from "rxjs";
|
|
4
4
|
import { TranslocoPipe, TranslocoService, provideTranslocoScope } from "@jsverse/transloco";
|
|
5
|
-
import { ArrowDownIcon, ArrowDownZaIcon, ArrowLeftIcon, ArrowRightFromBracketIcon, ArrowRightIcon, ArrowUpAzIcon, ArrowUpIcon, ArrowUpRightFromSquareIcon, ArrowsMaximizeIcon, AutocompleteComponent, AvatarComponent, AvatarFallbackComponent, AvatarImageComponent, BadgeComponent, BarsIcon, BellIcon, BellSlashIcon, BookmarkIcon, BreakpointObserverService, BuildingIcon, ButtonComponent, CalendarDayIcon, CheckIcon, CheckboxComponent, ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon, ChevronUpIcon, CircleCheckIcon, ClockIcon, CollapseIcon, CommentIcon, CommentsIcon, DIALOG_REF, DesktopIcon, DialogBodyComponent, DialogComponent, DialogContentComponent, DialogDescriptionComponent, DialogFooterComponent, DialogHeaderComponent, DialogTitleComponent, DropdownDirective, EditIcon, EllipsisIcon, EllipsisVerticalIcon, ExpandIcon, EyeIcon, EyeSlashIcon, FileHTMLIcon, FileIcon, FileOutputIcon, FileSpreadsheetIcon, FileTextIcon, FilterIcon, FlagEnglishIcon, FlagFrenchIcon, FlagGermanIcon, FlashlightIcon, FolderIcon, FolderOpenIcon, FrownIcon, GearIcon, GitForkIcon, GlobeIcon, GripVerticalIcon, HighlighterIcon, HistoryIcon, HomeIcon, ImageIcon, InboxIcon, InfoCircleIcon, InputComponent, InputControlDirective, InputIconRightDirective, KbdComponent, KbdGroupComponent, KeyIcon, KeyboardIcon, LanguageIcon, LightbulbIcon, LightbulbSlashIcon, LinkComponent, LinkIcon, ListComponent, ListItemActionComponent, ListItemAvatarDirective, ListItemComponent, ListItemIconDirective, ListItemLeftDirective, ListItemTextComponent, LoadingCircleIcon, LockIcon, MagnifyingGlassIcon, MagnifyingGlassMinusIcon, MagnifyingGlassPlusIcon, MapPinIcon, MenuComponent, MenuContentComponent, MenuItemComponent, MenuTriggerDirective, MoonIcon, PaletteIcon, PanelRightIcon, PaperclipIcon, PenToSquareIcon, PlusIcon, PopoverDirective, PopoverResizeHandleComponent, PresentationIcon, QuestionCircleIcon, QuickFilterIcon, RadioComponent, RobotIcon, SaveIcon, SearchBarComponent, SearchBarSuggestionsComponent, SearchIcon, SearchInputComponent, SelectComponent, SelectTriggerDirective, SendIcon, SheetBodyComponent, SheetComponent, SheetHeaderComponent, SidebarInsetDirective, SlashIcon, SlidersIcon, SortableDirective, SortableItemDirective, SparklesIcon, SpinnerIcon, SquareCheckIcon, SquareIcon, SquareMinusIcon, StarIcon, SunBrightIcon, SwitchComponent, SyncIcon, TabComponent, TabListComponent, TabPanelComponent, TabsComponent, TagComponent, TagIcon, TagInputComponent, TagsIcon, TextareaComponent, TextareaControlDirective, ThumbsDownIcon, ThumbsUpIcon, ToggleGroupComponent, ToggleItemComponent, ToggleLeftIcon, ToggleRightIcon, TooltipDirective, TrashCanIcon, TrashIcon, TreeComponent, TreeItemComponent, TreeItemExpanderComponent, TreeItemGroupComponent, TriangleAlertIcon, TruncatedDirective, UndoIcon, UserIcon, UserSecretIcon, VideoIcon, XMarkIcon, createCallable, injectCallRef, injectFloatingPanel, isApplePlatform } from "@sinequa/galactik";
|
|
5
|
+
import { ArrowDownIcon, ArrowDownZaIcon, ArrowLeftIcon, ArrowRightFromBracketIcon, ArrowRightIcon, ArrowUpAzIcon, ArrowUpIcon, ArrowUpRightFromSquareIcon, ArrowsMaximizeIcon, AutocompleteComponent, AvatarComponent, AvatarFallbackComponent, AvatarImageComponent, BadgeComponent, BarsIcon, BellIcon, BellSlashIcon, BookmarkIcon, BreakpointObserverService, BuildingIcon, ButtonComponent, CalendarDayIcon, CheckIcon, CheckboxComponent, ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon, ChevronUpIcon, CircleCheckIcon, ClockIcon, CollapseIcon, CommentIcon, CommentsIcon, DIALOG_REF, DesktopIcon, DialogBodyComponent, DialogComponent, DialogContentComponent, DialogDescriptionComponent, DialogFooterComponent, DialogHeaderComponent, DialogTitleComponent, DropdownDirective, EditIcon, EllipsisIcon, EllipsisVerticalIcon, ExpandIcon, EyeIcon, EyeSlashIcon, FileHTMLIcon, FileIcon, FileMagnifyingGlassIcon, FileOutputIcon, FileSpreadsheetIcon, FileTextIcon, FilesIcon, FilterIcon, FlagEnglishIcon, FlagFrenchIcon, FlagGermanIcon, FlashlightIcon, FolderIcon, FolderOpenIcon, FrownIcon, GearIcon, GitForkIcon, GlobeIcon, GripVerticalIcon, HighlighterIcon, HistoryIcon, HomeIcon, ImageIcon, InboxIcon, InfoCircleIcon, InputComponent, InputControlDirective, InputIconRightDirective, KbdComponent, KbdGroupComponent, KeyIcon, KeyboardIcon, LanguageIcon, LightbulbIcon, LightbulbSlashIcon, LinkComponent, LinkIcon, ListComponent, ListItemActionComponent, ListItemAvatarDirective, ListItemComponent, ListItemIconDirective, ListItemLeftDirective, ListItemTextComponent, LoadingCircleIcon, LockIcon, MagnifyingGlassIcon, MagnifyingGlassMinusIcon, MagnifyingGlassPlusIcon, MapPinIcon, MenuComponent, MenuContentComponent, MenuItemComponent, MenuTriggerDirective, MoonIcon, PaletteIcon, PanelRightIcon, PaperclipIcon, PenToSquareIcon, PlusIcon, PopoverDirective, PopoverResizeHandleComponent, PresentationIcon, QuestionCircleIcon, QuickFilterIcon, RadioComponent, RobotIcon, SaveIcon, SearchBarComponent, SearchBarSuggestionsComponent, SearchIcon, SearchInputComponent, SelectComponent, SelectTriggerDirective, SendIcon, SheetBodyComponent, SheetComponent, SheetHeaderComponent, SidebarInsetDirective, SlashIcon, SlidersIcon, SortableDirective, SortableItemDirective, SparklesIcon, SpinnerIcon, SquareCheckIcon, SquareIcon, SquareMinusIcon, StarIcon, SunBrightIcon, SwipeActionComponent, SwitchComponent, SyncIcon, TabComponent, TabListComponent, TabPanelComponent, TabsComponent, TagComponent, TagIcon, TagInputComponent, TagsIcon, TextareaComponent, TextareaControlDirective, ThumbsDownIcon, ThumbsUpIcon, ToggleGroupComponent, ToggleItemComponent, ToggleLeftIcon, ToggleRightIcon, TooltipDirective, TrashCanIcon, TrashIcon, TreeComponent, TreeItemComponent, TreeItemExpanderComponent, TreeItemGroupComponent, TriangleAlertIcon, TruncatedDirective, UndoIcon, UserIcon, UserSecretIcon, VideoIcon, XMarkIcon, createCallable, injectCallRef, injectFloatingPanel, injectKeyboardAvailable, isApplePlatform } from "@sinequa/galactik";
|
|
6
6
|
import { withDevtools } from "@ngrx-toolkit/core";
|
|
7
7
|
import { getState, patchState, signalStore, signalStoreFeature, type, withComputed, withMethods, withState } from "@ngrx/signals";
|
|
8
8
|
import { ActivatedRoute, NavigationEnd, Router, RouterLink, RouterModule } from "@angular/router";
|
|
@@ -4755,7 +4755,7 @@ var DocumentLocatorComponent = class DocumentLocatorComponent {
|
|
|
4755
4755
|
transformFunction: null
|
|
4756
4756
|
}
|
|
4757
4757
|
},
|
|
4758
|
-
host: { classAttribute: "flex items-center grow gap-2 overflow-hidden" },
|
|
4758
|
+
host: { classAttribute: "relative flex items-center grow gap-2 overflow-hidden" },
|
|
4759
4759
|
viewQueries: [{
|
|
4760
4760
|
propertyName: "shadow",
|
|
4761
4761
|
first: true,
|
|
@@ -4773,7 +4773,9 @@ var DocumentLocatorComponent = class DocumentLocatorComponent {
|
|
|
4773
4773
|
}],
|
|
4774
4774
|
ngImport: i0,
|
|
4775
4775
|
template: `
|
|
4776
|
-
<!-- Renders all segment hidden to user to compute width
|
|
4776
|
+
<!-- Renders all segment hidden to user to compute width. Absolute against the host, which is relative and clips
|
|
4777
|
+
it: positioned against some ancestor further up, its full width overflowed the page and gave it a horizontal
|
|
4778
|
+
scrollbar. -->
|
|
4777
4779
|
<div #shadowRender class="pointer-events-none invisible absolute left-0 top-0 -z-10 flex gap-1">
|
|
4778
4780
|
@for (segment of locationSegments(); track $index) {
|
|
4779
4781
|
<span class="whitespace-nowrap">{{ segment }}</span>
|
|
@@ -4788,7 +4790,9 @@ var DocumentLocatorComponent = class DocumentLocatorComponent {
|
|
|
4788
4790
|
|
|
4789
4791
|
<chevron-right-icon />
|
|
4790
4792
|
|
|
4791
|
-
|
|
4793
|
+
<!-- min-w-0: as a flex item its width otherwise never went below its content, so its clientWidth, which decides
|
|
4794
|
+
how many segments fit, did not follow a narrower host and the segments never folded into the menu. -->
|
|
4795
|
+
<div #documentLocator class="flex min-w-0 grow items-center gap-1">
|
|
4792
4796
|
@for (segment of visibleSegments(); track $index) {
|
|
4793
4797
|
<a link size="md" (click)="navigateToSegment($index)">{{segment }}</a>
|
|
4794
4798
|
|
|
@@ -4915,7 +4919,9 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
4915
4919
|
MenuItemComponent
|
|
4916
4920
|
],
|
|
4917
4921
|
template: `
|
|
4918
|
-
<!-- Renders all segment hidden to user to compute width
|
|
4922
|
+
<!-- Renders all segment hidden to user to compute width. Absolute against the host, which is relative and clips
|
|
4923
|
+
it: positioned against some ancestor further up, its full width overflowed the page and gave it a horizontal
|
|
4924
|
+
scrollbar. -->
|
|
4919
4925
|
<div #shadowRender class="pointer-events-none invisible absolute left-0 top-0 -z-10 flex gap-1">
|
|
4920
4926
|
@for (segment of locationSegments(); track $index) {
|
|
4921
4927
|
<span class="whitespace-nowrap">{{ segment }}</span>
|
|
@@ -4930,7 +4936,9 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
4930
4936
|
|
|
4931
4937
|
<chevron-right-icon />
|
|
4932
4938
|
|
|
4933
|
-
|
|
4939
|
+
<!-- min-w-0: as a flex item its width otherwise never went below its content, so its clientWidth, which decides
|
|
4940
|
+
how many segments fit, did not follow a narrower host and the segments never folded into the menu. -->
|
|
4941
|
+
<div #documentLocator class="flex min-w-0 grow items-center gap-1">
|
|
4934
4942
|
@for (segment of visibleSegments(); track $index) {
|
|
4935
4943
|
<a link size="md" (click)="navigateToSegment($index)">{{segment }}</a>
|
|
4936
4944
|
|
|
@@ -4960,7 +4968,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
4960
4968
|
}
|
|
4961
4969
|
</div>
|
|
4962
4970
|
`,
|
|
4963
|
-
host: { class: "flex items-center grow gap-2 overflow-hidden" }
|
|
4971
|
+
host: { class: "relative flex items-center grow gap-2 overflow-hidden" }
|
|
4964
4972
|
}]
|
|
4965
4973
|
}],
|
|
4966
4974
|
ctorParameters: () => [],
|
|
@@ -10272,8 +10280,205 @@ function injectSimilarDocumentsState(article, documents) {
|
|
|
10272
10280
|
hasContent: computed(() => similarDocuments().length > 0, ...ngDevMode ? [{ debugName: "hasContent" }] : /* istanbul ignore next */ [])
|
|
10273
10281
|
};
|
|
10274
10282
|
}
|
|
10283
|
+
const STEP_BUTTON = "inline-flex size-8 shrink-0 cursor-pointer touch-manipulation items-center justify-center rounded-(--radius-xs) border-0 bg-transparent p-0 text-(--font-primary-muted) transition-[background-color,color] duration-120 ease-[ease] motion-reduce:transition-none hover:bg-(--bg-primary-lighter) hover:text-(--font-primary-base) focus-visible:outline-2 focus-visible:outline-solid focus-visible:-outline-offset-2 focus-visible:outline-(--stroke-focus)";
|
|
10284
|
+
var OccurrenceNavigatorComponent = class OccurrenceNavigatorComponent {
|
|
10285
|
+
active = input(false, ...ngDevMode ? [{ debugName: "active" }] : /* istanbul ignore next */ []);
|
|
10286
|
+
index = input(0, ...ngDevMode ? [{ debugName: "index" }] : /* istanbul ignore next */ []);
|
|
10287
|
+
total = input(0, ...ngDevMode ? [{ debugName: "total" }] : /* istanbul ignore next */ []);
|
|
10288
|
+
previousLabel = input("previous occurrence", ...ngDevMode ? [{ debugName: "previousLabel" }] : /* istanbul ignore next */ []);
|
|
10289
|
+
nextLabel = input("next occurrence", ...ngDevMode ? [{ debugName: "nextLabel" }] : /* istanbul ignore next */ []);
|
|
10290
|
+
previous = output();
|
|
10291
|
+
next = output();
|
|
10292
|
+
stepButton = STEP_BUTTON;
|
|
10293
|
+
step(event, direction) {
|
|
10294
|
+
event.stopPropagation();
|
|
10295
|
+
if (direction === "previous") this.previous.emit();
|
|
10296
|
+
else this.next.emit();
|
|
10297
|
+
}
|
|
10298
|
+
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
10299
|
+
minVersion: "12.0.0",
|
|
10300
|
+
version: "22.2.1",
|
|
10301
|
+
ngImport: i0,
|
|
10302
|
+
type: OccurrenceNavigatorComponent,
|
|
10303
|
+
deps: [],
|
|
10304
|
+
target: i0.ɵɵFactoryTarget.Component
|
|
10305
|
+
});
|
|
10306
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({
|
|
10307
|
+
minVersion: "17.0.0",
|
|
10308
|
+
version: "22.2.1",
|
|
10309
|
+
type: OccurrenceNavigatorComponent,
|
|
10310
|
+
isStandalone: true,
|
|
10311
|
+
selector: "occurrence-navigator, OccurrenceNavigator",
|
|
10312
|
+
inputs: {
|
|
10313
|
+
active: {
|
|
10314
|
+
classPropertyName: "active",
|
|
10315
|
+
publicName: "active",
|
|
10316
|
+
isSignal: true,
|
|
10317
|
+
isRequired: false,
|
|
10318
|
+
transformFunction: null
|
|
10319
|
+
},
|
|
10320
|
+
index: {
|
|
10321
|
+
classPropertyName: "index",
|
|
10322
|
+
publicName: "index",
|
|
10323
|
+
isSignal: true,
|
|
10324
|
+
isRequired: false,
|
|
10325
|
+
transformFunction: null
|
|
10326
|
+
},
|
|
10327
|
+
total: {
|
|
10328
|
+
classPropertyName: "total",
|
|
10329
|
+
publicName: "total",
|
|
10330
|
+
isSignal: true,
|
|
10331
|
+
isRequired: false,
|
|
10332
|
+
transformFunction: null
|
|
10333
|
+
},
|
|
10334
|
+
previousLabel: {
|
|
10335
|
+
classPropertyName: "previousLabel",
|
|
10336
|
+
publicName: "previousLabel",
|
|
10337
|
+
isSignal: true,
|
|
10338
|
+
isRequired: false,
|
|
10339
|
+
transformFunction: null
|
|
10340
|
+
},
|
|
10341
|
+
nextLabel: {
|
|
10342
|
+
classPropertyName: "nextLabel",
|
|
10343
|
+
publicName: "nextLabel",
|
|
10344
|
+
isSignal: true,
|
|
10345
|
+
isRequired: false,
|
|
10346
|
+
transformFunction: null
|
|
10347
|
+
}
|
|
10348
|
+
},
|
|
10349
|
+
outputs: {
|
|
10350
|
+
previous: "previous",
|
|
10351
|
+
next: "next"
|
|
10352
|
+
},
|
|
10353
|
+
host: { classAttribute: "flex shrink-0 items-center gap-2 tabular-nums" },
|
|
10354
|
+
ngImport: i0,
|
|
10355
|
+
template: `
|
|
10356
|
+
@if (active()) {
|
|
10357
|
+
<button type="button" [class]="stepButton" [attr.aria-label]="previousLabel()" (click)="step($event, 'previous')">
|
|
10358
|
+
<chevron-left />
|
|
10359
|
+
</button>
|
|
10360
|
+
|
|
10361
|
+
<span class="min-w-12 text-center">{{ index() }} / {{ total() }}</span>
|
|
10362
|
+
|
|
10363
|
+
<button type="button" [class]="stepButton" [attr.aria-label]="nextLabel()" (click)="step($event, 'next')">
|
|
10364
|
+
<chevron-right />
|
|
10365
|
+
</button>
|
|
10366
|
+
} @else {
|
|
10367
|
+
{{ total() }}
|
|
10368
|
+
}
|
|
10369
|
+
`,
|
|
10370
|
+
isInline: true,
|
|
10371
|
+
dependencies: [{
|
|
10372
|
+
kind: "component",
|
|
10373
|
+
type: ChevronLeftIcon,
|
|
10374
|
+
selector: "chevron-left, chevron-left-icon, ChevronLeft, ChevronLeftIcon",
|
|
10375
|
+
inputs: ["class"]
|
|
10376
|
+
}, {
|
|
10377
|
+
kind: "component",
|
|
10378
|
+
type: ChevronRightIcon,
|
|
10379
|
+
selector: "chevron-right, chevron-right-icon, ChevronRight, ChevronRightIcon",
|
|
10380
|
+
inputs: ["class"]
|
|
10381
|
+
}],
|
|
10382
|
+
changeDetection: i0.ChangeDetectionStrategy.OnPush
|
|
10383
|
+
});
|
|
10384
|
+
};
|
|
10385
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
10386
|
+
minVersion: "12.0.0",
|
|
10387
|
+
version: "22.2.1",
|
|
10388
|
+
ngImport: i0,
|
|
10389
|
+
type: OccurrenceNavigatorComponent,
|
|
10390
|
+
decorators: [{
|
|
10391
|
+
type: Component,
|
|
10392
|
+
args: [{
|
|
10393
|
+
selector: "occurrence-navigator, OccurrenceNavigator",
|
|
10394
|
+
imports: [ChevronLeftIcon, ChevronRightIcon],
|
|
10395
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
10396
|
+
host: { class: "flex shrink-0 items-center gap-2 tabular-nums" },
|
|
10397
|
+
template: `
|
|
10398
|
+
@if (active()) {
|
|
10399
|
+
<button type="button" [class]="stepButton" [attr.aria-label]="previousLabel()" (click)="step($event, 'previous')">
|
|
10400
|
+
<chevron-left />
|
|
10401
|
+
</button>
|
|
10402
|
+
|
|
10403
|
+
<span class="min-w-12 text-center">{{ index() }} / {{ total() }}</span>
|
|
10404
|
+
|
|
10405
|
+
<button type="button" [class]="stepButton" [attr.aria-label]="nextLabel()" (click)="step($event, 'next')">
|
|
10406
|
+
<chevron-right />
|
|
10407
|
+
</button>
|
|
10408
|
+
} @else {
|
|
10409
|
+
{{ total() }}
|
|
10410
|
+
}
|
|
10411
|
+
`
|
|
10412
|
+
}]
|
|
10413
|
+
}],
|
|
10414
|
+
propDecorators: {
|
|
10415
|
+
active: [{
|
|
10416
|
+
type: i0.Input,
|
|
10417
|
+
args: [{
|
|
10418
|
+
isSignal: true,
|
|
10419
|
+
alias: "active",
|
|
10420
|
+
required: false
|
|
10421
|
+
}]
|
|
10422
|
+
}],
|
|
10423
|
+
index: [{
|
|
10424
|
+
type: i0.Input,
|
|
10425
|
+
args: [{
|
|
10426
|
+
isSignal: true,
|
|
10427
|
+
alias: "index",
|
|
10428
|
+
required: false
|
|
10429
|
+
}]
|
|
10430
|
+
}],
|
|
10431
|
+
total: [{
|
|
10432
|
+
type: i0.Input,
|
|
10433
|
+
args: [{
|
|
10434
|
+
isSignal: true,
|
|
10435
|
+
alias: "total",
|
|
10436
|
+
required: false
|
|
10437
|
+
}]
|
|
10438
|
+
}],
|
|
10439
|
+
previousLabel: [{
|
|
10440
|
+
type: i0.Input,
|
|
10441
|
+
args: [{
|
|
10442
|
+
isSignal: true,
|
|
10443
|
+
alias: "previousLabel",
|
|
10444
|
+
required: false
|
|
10445
|
+
}]
|
|
10446
|
+
}],
|
|
10447
|
+
nextLabel: [{
|
|
10448
|
+
type: i0.Input,
|
|
10449
|
+
args: [{
|
|
10450
|
+
isSignal: true,
|
|
10451
|
+
alias: "nextLabel",
|
|
10452
|
+
required: false
|
|
10453
|
+
}]
|
|
10454
|
+
}],
|
|
10455
|
+
previous: [{
|
|
10456
|
+
type: i0.Output,
|
|
10457
|
+
args: ["previous"]
|
|
10458
|
+
}],
|
|
10459
|
+
next: [{
|
|
10460
|
+
type: i0.Output,
|
|
10461
|
+
args: ["next"]
|
|
10462
|
+
}]
|
|
10463
|
+
}
|
|
10464
|
+
});
|
|
10465
|
+
function injectRowActivation(active) {
|
|
10466
|
+
const isTouch = inject(BreakpointObserverService).isTouch;
|
|
10467
|
+
return {
|
|
10468
|
+
enter: (value) => {
|
|
10469
|
+
if (!isTouch()) active().set(value);
|
|
10470
|
+
},
|
|
10471
|
+
leave: () => {
|
|
10472
|
+
if (!isTouch()) active().set(void 0);
|
|
10473
|
+
},
|
|
10474
|
+
activate: (value) => {
|
|
10475
|
+
if (isTouch()) active().set(value);
|
|
10476
|
+
}
|
|
10477
|
+
};
|
|
10478
|
+
}
|
|
10275
10479
|
var ArticleEntitiesTabPanel = class ArticleEntitiesTabPanel {
|
|
10276
10480
|
state = input.required(...ngDevMode ? [{ debugName: "state" }] : /* istanbul ignore next */ []);
|
|
10481
|
+
row = injectRowActivation(() => this.state().hovering);
|
|
10277
10482
|
expandedCategories = signal({}, ...ngDevMode ? [{ debugName: "expandedCategories" }] : /* istanbul ignore next */ []);
|
|
10278
10483
|
isExpanded(entity) {
|
|
10279
10484
|
return this.expandedCategories()[entity] ?? false;
|
|
@@ -10324,12 +10529,14 @@ var ArticleEntitiesTabPanel = class ArticleEntitiesTabPanel {
|
|
|
10324
10529
|
} },
|
|
10325
10530
|
ngImport: i0,
|
|
10326
10531
|
template: `
|
|
10532
|
+
@if (state().hasContent()) {
|
|
10327
10533
|
<!-- Legend, once for the whole tree — not repeated per row (aria-hidden: sighted-keyboard-user
|
|
10328
|
-
hint only, the buttons on each row already carry the real accessible names).
|
|
10534
|
+
hint only, the buttons on each row already carry the real accessible names). Hidden on a touch
|
|
10535
|
+
screen: there is no keyboard to press the keys on. -->
|
|
10329
10536
|
<div
|
|
10330
10537
|
data-name="shortcuts-legend"
|
|
10331
10538
|
aria-hidden="true"
|
|
10332
|
-
class="mb-1 flex items-center gap-1.5 px-2 py-1 text-[10px] text-(--font-info-base)">
|
|
10539
|
+
class="mb-1 flex items-center gap-1.5 px-2 py-1 text-[10px] text-(--font-info-base) pointer-coarse:hidden">
|
|
10333
10540
|
<KbdGroup class="text-[10px]">
|
|
10334
10541
|
<kbd size="xs" class="text-[10px]">Ctrl</kbd>
|
|
10335
10542
|
<kbd size="xs" class="text-[10px]"><arrow-left class="size-2.5" /></kbd>
|
|
@@ -10354,7 +10561,7 @@ var ArticleEntitiesTabPanel = class ArticleEntitiesTabPanel {
|
|
|
10354
10561
|
[attr.aria-label]="categoryLabel"
|
|
10355
10562
|
[expanded]="isExpanded(highlight.entity)"
|
|
10356
10563
|
(expandedChange)="setExpanded(highlight.entity, $event)"
|
|
10357
|
-
class="text-(length:--size-
|
|
10564
|
+
class="min-h-10 text-(length:--size-lg) leading-(--line-height-3) font-semibold capitalize">
|
|
10358
10565
|
<TreeItemExpander />
|
|
10359
10566
|
<span class="inline-block size-3 shrink-0 rounded-full" [style.background]="highlight.color"></span>
|
|
10360
10567
|
{{ categoryLabel }}
|
|
@@ -10371,30 +10578,25 @@ var ArticleEntitiesTabPanel = class ArticleEntitiesTabPanel {
|
|
|
10371
10578
|
[value]="highlight.entity + ':' + data.value"
|
|
10372
10579
|
[label]="data.display"
|
|
10373
10580
|
[attr.aria-label]="data.display"
|
|
10374
|
-
class="text-(length:--size-
|
|
10375
|
-
(mouseenter)="
|
|
10376
|
-
(mouseleave)="
|
|
10581
|
+
class="min-h-10 text-(length:--size-lg) leading-(--line-height-3) select-none"
|
|
10582
|
+
(mouseenter)="row.enter(data.value)"
|
|
10583
|
+
(mouseleave)="row.leave()"
|
|
10584
|
+
(click)="row.activate(data.value)"
|
|
10377
10585
|
(focus)="focusedLeaf.set(data.value)"
|
|
10378
10586
|
(blur)="onLeafBlur(data.value)"
|
|
10379
10587
|
(keydown.control.arrowleft)="onNavigateKey($event, highlight.entity, data, 'prev')"
|
|
10380
10588
|
(keydown.control.arrowright)="onNavigateKey($event, highlight.entity, data, 'next')">
|
|
10381
10589
|
{{ data.display }}
|
|
10382
10590
|
|
|
10383
|
-
<
|
|
10384
|
-
|
|
10385
|
-
|
|
10386
|
-
|
|
10387
|
-
|
|
10388
|
-
|
|
10389
|
-
|
|
10390
|
-
|
|
10391
|
-
|
|
10392
|
-
<chevron-right />
|
|
10393
|
-
</button>
|
|
10394
|
-
} @else {
|
|
10395
|
-
{{ state().total(highlight.entity, data) }}
|
|
10396
|
-
}
|
|
10397
|
-
</span>
|
|
10591
|
+
<occurrence-navigator
|
|
10592
|
+
class="ml-auto text-(length:--size-sm)"
|
|
10593
|
+
[active]="isLeafActive(data.value)"
|
|
10594
|
+
[index]="currentIndex(data)"
|
|
10595
|
+
[total]="state().total(highlight.entity, data)"
|
|
10596
|
+
previousLabel="previous entity"
|
|
10597
|
+
nextLabel="next entity"
|
|
10598
|
+
(previous)="state().navigatePrev(highlight.entity, data)"
|
|
10599
|
+
(next)="state().navigateNext(highlight.entity, data)" />
|
|
10398
10600
|
</TreeItem>
|
|
10399
10601
|
}
|
|
10400
10602
|
</div>
|
|
@@ -10402,6 +10604,14 @@ var ArticleEntitiesTabPanel = class ArticleEntitiesTabPanel {
|
|
|
10402
10604
|
}
|
|
10403
10605
|
}
|
|
10404
10606
|
</Tree>
|
|
10607
|
+
} @else {
|
|
10608
|
+
<!-- Not reached through the tabs (the Entities tab only exists once a category has a value), but a host may mount it on its own. -->
|
|
10609
|
+
<list-panel-empty color="pink">
|
|
10610
|
+
<building-icon listPanelEmptyIcon />
|
|
10611
|
+
{{ 'drawers.noEntities' | transloco }}
|
|
10612
|
+
<p listPanelEmptyHint>{{ 'drawers.noEntitiesHint' | transloco }}</p>
|
|
10613
|
+
</list-panel-empty>
|
|
10614
|
+
}
|
|
10405
10615
|
`,
|
|
10406
10616
|
isInline: true,
|
|
10407
10617
|
dependencies: [
|
|
@@ -10444,27 +10654,40 @@ var ArticleEntitiesTabPanel = class ArticleEntitiesTabPanel {
|
|
|
10444
10654
|
},
|
|
10445
10655
|
{
|
|
10446
10656
|
kind: "component",
|
|
10447
|
-
type:
|
|
10448
|
-
selector: "
|
|
10657
|
+
type: ArrowLeftIcon,
|
|
10658
|
+
selector: "arrow-left, arrow-left-icon, ArrowLeft, ArrowLeftIcon",
|
|
10449
10659
|
inputs: ["class"]
|
|
10450
10660
|
},
|
|
10451
10661
|
{
|
|
10452
10662
|
kind: "component",
|
|
10453
|
-
type:
|
|
10454
|
-
selector: "
|
|
10663
|
+
type: ArrowRightIcon,
|
|
10664
|
+
selector: "arrow-right, arrow-right-icon, ArrowRight, ArrowRightIcon",
|
|
10455
10665
|
inputs: ["class"]
|
|
10456
10666
|
},
|
|
10457
10667
|
{
|
|
10458
10668
|
kind: "component",
|
|
10459
|
-
type:
|
|
10460
|
-
selector: "
|
|
10669
|
+
type: BuildingIcon,
|
|
10670
|
+
selector: "building, building-icon, Building, BuildingIcon",
|
|
10461
10671
|
inputs: ["class"]
|
|
10462
10672
|
},
|
|
10463
10673
|
{
|
|
10464
10674
|
kind: "component",
|
|
10465
|
-
type:
|
|
10466
|
-
selector: "
|
|
10467
|
-
inputs: ["
|
|
10675
|
+
type: ListPanelEmptyComponent,
|
|
10676
|
+
selector: "list-panel-empty, ListPanelEmpty",
|
|
10677
|
+
inputs: ["color"]
|
|
10678
|
+
},
|
|
10679
|
+
{
|
|
10680
|
+
kind: "component",
|
|
10681
|
+
type: OccurrenceNavigatorComponent,
|
|
10682
|
+
selector: "occurrence-navigator, OccurrenceNavigator",
|
|
10683
|
+
inputs: [
|
|
10684
|
+
"active",
|
|
10685
|
+
"index",
|
|
10686
|
+
"total",
|
|
10687
|
+
"previousLabel",
|
|
10688
|
+
"nextLabel"
|
|
10689
|
+
],
|
|
10690
|
+
outputs: ["previous", "next"]
|
|
10468
10691
|
},
|
|
10469
10692
|
{
|
|
10470
10693
|
kind: "pipe",
|
|
@@ -10491,18 +10714,21 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
10491
10714
|
TreeItemExpanderComponent,
|
|
10492
10715
|
KbdComponent,
|
|
10493
10716
|
KbdGroupComponent,
|
|
10494
|
-
ChevronRightIcon,
|
|
10495
|
-
ChevronLeftIcon,
|
|
10496
10717
|
ArrowLeftIcon,
|
|
10497
|
-
ArrowRightIcon
|
|
10718
|
+
ArrowRightIcon,
|
|
10719
|
+
BuildingIcon,
|
|
10720
|
+
ListPanelEmptyComponent,
|
|
10721
|
+
OccurrenceNavigatorComponent
|
|
10498
10722
|
],
|
|
10499
10723
|
template: `
|
|
10724
|
+
@if (state().hasContent()) {
|
|
10500
10725
|
<!-- Legend, once for the whole tree — not repeated per row (aria-hidden: sighted-keyboard-user
|
|
10501
|
-
hint only, the buttons on each row already carry the real accessible names).
|
|
10726
|
+
hint only, the buttons on each row already carry the real accessible names). Hidden on a touch
|
|
10727
|
+
screen: there is no keyboard to press the keys on. -->
|
|
10502
10728
|
<div
|
|
10503
10729
|
data-name="shortcuts-legend"
|
|
10504
10730
|
aria-hidden="true"
|
|
10505
|
-
class="mb-1 flex items-center gap-1.5 px-2 py-1 text-[10px] text-(--font-info-base)">
|
|
10731
|
+
class="mb-1 flex items-center gap-1.5 px-2 py-1 text-[10px] text-(--font-info-base) pointer-coarse:hidden">
|
|
10506
10732
|
<KbdGroup class="text-[10px]">
|
|
10507
10733
|
<kbd size="xs" class="text-[10px]">Ctrl</kbd>
|
|
10508
10734
|
<kbd size="xs" class="text-[10px]"><arrow-left class="size-2.5" /></kbd>
|
|
@@ -10527,7 +10753,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
10527
10753
|
[attr.aria-label]="categoryLabel"
|
|
10528
10754
|
[expanded]="isExpanded(highlight.entity)"
|
|
10529
10755
|
(expandedChange)="setExpanded(highlight.entity, $event)"
|
|
10530
|
-
class="text-(length:--size-
|
|
10756
|
+
class="min-h-10 text-(length:--size-lg) leading-(--line-height-3) font-semibold capitalize">
|
|
10531
10757
|
<TreeItemExpander />
|
|
10532
10758
|
<span class="inline-block size-3 shrink-0 rounded-full" [style.background]="highlight.color"></span>
|
|
10533
10759
|
{{ categoryLabel }}
|
|
@@ -10544,30 +10770,25 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
10544
10770
|
[value]="highlight.entity + ':' + data.value"
|
|
10545
10771
|
[label]="data.display"
|
|
10546
10772
|
[attr.aria-label]="data.display"
|
|
10547
|
-
class="text-(length:--size-
|
|
10548
|
-
(mouseenter)="
|
|
10549
|
-
(mouseleave)="
|
|
10773
|
+
class="min-h-10 text-(length:--size-lg) leading-(--line-height-3) select-none"
|
|
10774
|
+
(mouseenter)="row.enter(data.value)"
|
|
10775
|
+
(mouseleave)="row.leave()"
|
|
10776
|
+
(click)="row.activate(data.value)"
|
|
10550
10777
|
(focus)="focusedLeaf.set(data.value)"
|
|
10551
10778
|
(blur)="onLeafBlur(data.value)"
|
|
10552
10779
|
(keydown.control.arrowleft)="onNavigateKey($event, highlight.entity, data, 'prev')"
|
|
10553
10780
|
(keydown.control.arrowright)="onNavigateKey($event, highlight.entity, data, 'next')">
|
|
10554
10781
|
{{ data.display }}
|
|
10555
10782
|
|
|
10556
|
-
<
|
|
10557
|
-
|
|
10558
|
-
|
|
10559
|
-
|
|
10560
|
-
|
|
10561
|
-
|
|
10562
|
-
|
|
10563
|
-
|
|
10564
|
-
|
|
10565
|
-
<chevron-right />
|
|
10566
|
-
</button>
|
|
10567
|
-
} @else {
|
|
10568
|
-
{{ state().total(highlight.entity, data) }}
|
|
10569
|
-
}
|
|
10570
|
-
</span>
|
|
10783
|
+
<occurrence-navigator
|
|
10784
|
+
class="ml-auto text-(length:--size-sm)"
|
|
10785
|
+
[active]="isLeafActive(data.value)"
|
|
10786
|
+
[index]="currentIndex(data)"
|
|
10787
|
+
[total]="state().total(highlight.entity, data)"
|
|
10788
|
+
previousLabel="previous entity"
|
|
10789
|
+
nextLabel="next entity"
|
|
10790
|
+
(previous)="state().navigatePrev(highlight.entity, data)"
|
|
10791
|
+
(next)="state().navigateNext(highlight.entity, data)" />
|
|
10571
10792
|
</TreeItem>
|
|
10572
10793
|
}
|
|
10573
10794
|
</div>
|
|
@@ -10575,6 +10796,14 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
10575
10796
|
}
|
|
10576
10797
|
}
|
|
10577
10798
|
</Tree>
|
|
10799
|
+
} @else {
|
|
10800
|
+
<!-- Not reached through the tabs (the Entities tab only exists once a category has a value), but a host may mount it on its own. -->
|
|
10801
|
+
<list-panel-empty color="pink">
|
|
10802
|
+
<building-icon listPanelEmptyIcon />
|
|
10803
|
+
{{ 'drawers.noEntities' | transloco }}
|
|
10804
|
+
<p listPanelEmptyHint>{{ 'drawers.noEntitiesHint' | transloco }}</p>
|
|
10805
|
+
</list-panel-empty>
|
|
10806
|
+
}
|
|
10578
10807
|
`
|
|
10579
10808
|
}]
|
|
10580
10809
|
}],
|
|
@@ -10616,6 +10845,13 @@ var ArticleExtractsTabPanel = class ArticleExtractsTabPanel {
|
|
|
10616
10845
|
<div class="flex h-32 items-center justify-center">
|
|
10617
10846
|
<loading-dots size="md" class="text-(--font-primary-base)" [label]="'drawers.loadingExtracts' | transloco" />
|
|
10618
10847
|
</div>
|
|
10848
|
+
} @else if (state().extracts().length === 0) {
|
|
10849
|
+
<!-- The empty-state rule of the list features: the feature's icon on a coloured tile, a title, one hint line. -->
|
|
10850
|
+
<list-panel-empty color="green">
|
|
10851
|
+
<file-text-icon listPanelEmptyIcon />
|
|
10852
|
+
{{ 'drawers.noRelevantExtracts' | transloco }}
|
|
10853
|
+
<p listPanelEmptyHint>{{ 'drawers.noRelevantExtractsHint' | transloco }}</p>
|
|
10854
|
+
</list-panel-empty>
|
|
10619
10855
|
} @else {
|
|
10620
10856
|
<List class="gap-1" selectionMode="explicit">
|
|
10621
10857
|
@for (extract of state().extracts(); track extract.id) {
|
|
@@ -10627,16 +10863,18 @@ var ArticleExtractsTabPanel = class ArticleExtractsTabPanel {
|
|
|
10627
10863
|
(keydown.space)="state().scrollTo(undefined, extract.textIndex, true)">
|
|
10628
10864
|
<p class="line-clamp-5 text-(length:--size-base) leading-(--line-height-3)" [innerHTML]="extract.text"></p>
|
|
10629
10865
|
</ListItem>
|
|
10630
|
-
} @empty {
|
|
10631
|
-
<!-- div, not p: a <p> picks up the app's own paragraph typography (font-size, margin),
|
|
10632
|
-
drifting from the row size above it — same fix as article-entities-tab-panel.ts. -->
|
|
10633
|
-
<div class="px-3 py-1 text-(length:--size-base) text-(--font-neutral-secondary) italic">{{ 'drawers.noRelevantExtracts' | transloco }}</div>
|
|
10634
10866
|
}
|
|
10635
10867
|
</List>
|
|
10636
10868
|
}
|
|
10637
10869
|
`,
|
|
10638
10870
|
isInline: true,
|
|
10639
10871
|
dependencies: [
|
|
10872
|
+
{
|
|
10873
|
+
kind: "component",
|
|
10874
|
+
type: FileTextIcon,
|
|
10875
|
+
selector: "file-text, file-text-icon, FileText, FileTextIcon",
|
|
10876
|
+
inputs: ["class"]
|
|
10877
|
+
},
|
|
10640
10878
|
{
|
|
10641
10879
|
kind: "directive",
|
|
10642
10880
|
type: ListComponent,
|
|
@@ -10660,6 +10898,12 @@ var ArticleExtractsTabPanel = class ArticleExtractsTabPanel {
|
|
|
10660
10898
|
],
|
|
10661
10899
|
exportAs: ["listItem"]
|
|
10662
10900
|
},
|
|
10901
|
+
{
|
|
10902
|
+
kind: "component",
|
|
10903
|
+
type: ListPanelEmptyComponent,
|
|
10904
|
+
selector: "list-panel-empty, ListPanelEmpty",
|
|
10905
|
+
inputs: ["color"]
|
|
10906
|
+
},
|
|
10663
10907
|
{
|
|
10664
10908
|
kind: "component",
|
|
10665
10909
|
type: LoadingDotsComponent,
|
|
@@ -10685,8 +10929,10 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
10685
10929
|
selector: "article-extracts-tab-panel, ArticleExtractsTabPanel, articleextractstabpanel",
|
|
10686
10930
|
imports: [
|
|
10687
10931
|
TranslocoPipe,
|
|
10932
|
+
FileTextIcon,
|
|
10688
10933
|
ListComponent,
|
|
10689
10934
|
ListItemComponent,
|
|
10935
|
+
ListPanelEmptyComponent,
|
|
10690
10936
|
LoadingDotsComponent
|
|
10691
10937
|
],
|
|
10692
10938
|
template: `
|
|
@@ -10694,6 +10940,13 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
10694
10940
|
<div class="flex h-32 items-center justify-center">
|
|
10695
10941
|
<loading-dots size="md" class="text-(--font-primary-base)" [label]="'drawers.loadingExtracts' | transloco" />
|
|
10696
10942
|
</div>
|
|
10943
|
+
} @else if (state().extracts().length === 0) {
|
|
10944
|
+
<!-- The empty-state rule of the list features: the feature's icon on a coloured tile, a title, one hint line. -->
|
|
10945
|
+
<list-panel-empty color="green">
|
|
10946
|
+
<file-text-icon listPanelEmptyIcon />
|
|
10947
|
+
{{ 'drawers.noRelevantExtracts' | transloco }}
|
|
10948
|
+
<p listPanelEmptyHint>{{ 'drawers.noRelevantExtractsHint' | transloco }}</p>
|
|
10949
|
+
</list-panel-empty>
|
|
10697
10950
|
} @else {
|
|
10698
10951
|
<List class="gap-1" selectionMode="explicit">
|
|
10699
10952
|
@for (extract of state().extracts(); track extract.id) {
|
|
@@ -10705,10 +10958,6 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
10705
10958
|
(keydown.space)="state().scrollTo(undefined, extract.textIndex, true)">
|
|
10706
10959
|
<p class="line-clamp-5 text-(length:--size-base) leading-(--line-height-3)" [innerHTML]="extract.text"></p>
|
|
10707
10960
|
</ListItem>
|
|
10708
|
-
} @empty {
|
|
10709
|
-
<!-- div, not p: a <p> picks up the app's own paragraph typography (font-size, margin),
|
|
10710
|
-
drifting from the row size above it — same fix as article-entities-tab-panel.ts. -->
|
|
10711
|
-
<div class="px-3 py-1 text-(length:--size-base) text-(--font-neutral-secondary) italic">{{ 'drawers.noRelevantExtracts' | transloco }}</div>
|
|
10712
10961
|
}
|
|
10713
10962
|
</List>
|
|
10714
10963
|
}
|
|
@@ -10861,6 +11110,8 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
10861
11110
|
});
|
|
10862
11111
|
var ArticleMatchLocationsTabPanel = class ArticleMatchLocationsTabPanel {
|
|
10863
11112
|
state = input.required(...ngDevMode ? [{ debugName: "state" }] : /* istanbul ignore next */ []);
|
|
11113
|
+
queryText = input("", ...ngDevMode ? [{ debugName: "queryText" }] : /* istanbul ignore next */ []);
|
|
11114
|
+
row = injectRowActivation(() => this.state().hovering);
|
|
10864
11115
|
focusedValue = signal(void 0, ...ngDevMode ? [{ debugName: "focusedValue" }] : /* istanbul ignore next */ []);
|
|
10865
11116
|
onBlur(value) {
|
|
10866
11117
|
if (this.focusedValue() === value) this.focusedValue.set(void 0);
|
|
@@ -10892,21 +11143,32 @@ var ArticleMatchLocationsTabPanel = class ArticleMatchLocationsTabPanel {
|
|
|
10892
11143
|
type: ArticleMatchLocationsTabPanel,
|
|
10893
11144
|
isStandalone: true,
|
|
10894
11145
|
selector: "article-match-locations-tab-panel, ArticleMatchLocationsTabPanel, articlematchlocationstabpanel",
|
|
10895
|
-
inputs: {
|
|
10896
|
-
|
|
10897
|
-
|
|
10898
|
-
|
|
10899
|
-
|
|
10900
|
-
|
|
10901
|
-
|
|
11146
|
+
inputs: {
|
|
11147
|
+
state: {
|
|
11148
|
+
classPropertyName: "state",
|
|
11149
|
+
publicName: "state",
|
|
11150
|
+
isSignal: true,
|
|
11151
|
+
isRequired: true,
|
|
11152
|
+
transformFunction: null
|
|
11153
|
+
},
|
|
11154
|
+
queryText: {
|
|
11155
|
+
classPropertyName: "queryText",
|
|
11156
|
+
publicName: "queryText",
|
|
11157
|
+
isSignal: true,
|
|
11158
|
+
isRequired: false,
|
|
11159
|
+
transformFunction: null
|
|
11160
|
+
}
|
|
11161
|
+
},
|
|
10902
11162
|
ngImport: i0,
|
|
10903
11163
|
template: `
|
|
11164
|
+
@if (state().hasContent()) {
|
|
10904
11165
|
<!-- Legend, once for the whole list — not repeated per row (aria-hidden: sighted-keyboard-user
|
|
10905
|
-
hint only, the buttons on each row already carry the real accessible names).
|
|
11166
|
+
hint only, the buttons on each row already carry the real accessible names). Hidden on a touch
|
|
11167
|
+
screen: there is no keyboard to press the keys on. -->
|
|
10906
11168
|
<div
|
|
10907
11169
|
data-name="shortcuts-legend"
|
|
10908
11170
|
aria-hidden="true"
|
|
10909
|
-
class="mb-1 flex items-center gap-1.5 px-2 py-1 text-[10px] text-(--font-info-base)">
|
|
11171
|
+
class="mb-1 flex items-center gap-1.5 px-2 py-1 text-[10px] text-(--font-info-base) pointer-coarse:hidden">
|
|
10910
11172
|
<KbdGroup class="text-[10px]">
|
|
10911
11173
|
<kbd size="xs" class="text-[10px]">Ctrl</kbd>
|
|
10912
11174
|
<kbd size="xs" class="text-[10px]"><arrow-left class="size-2.5" /></kbd>
|
|
@@ -10919,38 +11181,50 @@ var ArticleMatchLocationsTabPanel = class ArticleMatchLocationsTabPanel {
|
|
|
10919
11181
|
<span>{{ 'drawers.navigateOccurrences' | transloco }}</span>
|
|
10920
11182
|
</div>
|
|
10921
11183
|
|
|
10922
|
-
<List variant="list-menu" class="gap-
|
|
11184
|
+
<List variant="list-menu" class="gap-2" selectionMode="explicit">
|
|
10923
11185
|
@for (data of state().values(); track data.value) {
|
|
10924
11186
|
<ListItem
|
|
10925
11187
|
size="small"
|
|
10926
11188
|
[value]="data.value"
|
|
10927
|
-
class="select-none"
|
|
10928
|
-
(mouseenter)="
|
|
10929
|
-
(mouseleave)="
|
|
11189
|
+
class="select-none min-h-10 [&_.list-item-text]:text-(length:--size-lg)"
|
|
11190
|
+
(mouseenter)="row.enter(data.value)"
|
|
11191
|
+
(mouseleave)="row.leave()"
|
|
11192
|
+
(click)="row.activate(data.value)"
|
|
10930
11193
|
(focus)="focusedValue.set(data.value)"
|
|
10931
11194
|
(blur)="onBlur(data.value)"
|
|
10932
11195
|
(keydown.control.arrowleft)="onNavigateKey($event, data, 'prev')"
|
|
10933
11196
|
(keydown.control.arrowright)="onNavigateKey($event, data, 'next')">
|
|
10934
11197
|
<ListItemText>{{ data.display }}</ListItemText>
|
|
10935
11198
|
|
|
10936
|
-
<
|
|
10937
|
-
|
|
10938
|
-
|
|
10939
|
-
|
|
10940
|
-
|
|
10941
|
-
|
|
10942
|
-
|
|
10943
|
-
|
|
10944
|
-
|
|
10945
|
-
<chevron-right />
|
|
10946
|
-
</button>
|
|
10947
|
-
} @else {
|
|
10948
|
-
{{ data.count }}
|
|
10949
|
-
}
|
|
10950
|
-
</span>
|
|
11199
|
+
<occurrence-navigator
|
|
11200
|
+
class="ml-auto"
|
|
11201
|
+
[active]="isActive(data.value)"
|
|
11202
|
+
[index]="currentIndex(data)"
|
|
11203
|
+
[total]="data.count ?? 0"
|
|
11204
|
+
previousLabel="previous match"
|
|
11205
|
+
nextLabel="next match"
|
|
11206
|
+
(previous)="state().navigatePrev(data)"
|
|
11207
|
+
(next)="state().navigateNext(data)" />
|
|
10951
11208
|
</ListItem>
|
|
10952
11209
|
}
|
|
10953
11210
|
</List>
|
|
11211
|
+
} @else {
|
|
11212
|
+
<!-- With nothing to show it says why: no search yet, or no match. Two whole empty states rather than one with an @if inside:
|
|
11213
|
+
the hint is projected into its slot only when it is a direct child of <list-panel-empty> (NG8011 otherwise). -->
|
|
11214
|
+
@if (queryText()) {
|
|
11215
|
+
<list-panel-empty color="blue">
|
|
11216
|
+
<file-magnifying-glass-icon listPanelEmptyIcon />
|
|
11217
|
+
{{ 'drawers.noMatches' | transloco: { query: queryText() } }}
|
|
11218
|
+
<p listPanelEmptyHint>{{ 'drawers.noMatchesHint' | transloco }}</p>
|
|
11219
|
+
</list-panel-empty>
|
|
11220
|
+
} @else {
|
|
11221
|
+
<list-panel-empty color="blue">
|
|
11222
|
+
<file-magnifying-glass-icon listPanelEmptyIcon />
|
|
11223
|
+
{{ 'drawers.typeToSearch' | transloco }}
|
|
11224
|
+
<p listPanelEmptyHint>{{ 'drawers.typeToSearchHint' | transloco }}</p>
|
|
11225
|
+
</list-panel-empty>
|
|
11226
|
+
}
|
|
11227
|
+
}
|
|
10954
11228
|
`,
|
|
10955
11229
|
isInline: true,
|
|
10956
11230
|
dependencies: [
|
|
@@ -10984,18 +11258,6 @@ var ArticleMatchLocationsTabPanel = class ArticleMatchLocationsTabPanel {
|
|
|
10984
11258
|
inputs: ["class", "subtext"],
|
|
10985
11259
|
exportAs: ["listItemText"]
|
|
10986
11260
|
},
|
|
10987
|
-
{
|
|
10988
|
-
kind: "component",
|
|
10989
|
-
type: ChevronLeftIcon,
|
|
10990
|
-
selector: "chevron-left, chevron-left-icon, ChevronLeft, ChevronLeftIcon",
|
|
10991
|
-
inputs: ["class"]
|
|
10992
|
-
},
|
|
10993
|
-
{
|
|
10994
|
-
kind: "component",
|
|
10995
|
-
type: ChevronRightIcon,
|
|
10996
|
-
selector: "chevron-right, chevron-right-icon, ChevronRight, ChevronRightIcon",
|
|
10997
|
-
inputs: ["class"]
|
|
10998
|
-
},
|
|
10999
11261
|
{
|
|
11000
11262
|
kind: "directive",
|
|
11001
11263
|
type: KbdComponent,
|
|
@@ -11019,6 +11281,31 @@ var ArticleMatchLocationsTabPanel = class ArticleMatchLocationsTabPanel {
|
|
|
11019
11281
|
selector: "arrow-right, arrow-right-icon, ArrowRight, ArrowRightIcon",
|
|
11020
11282
|
inputs: ["class"]
|
|
11021
11283
|
},
|
|
11284
|
+
{
|
|
11285
|
+
kind: "component",
|
|
11286
|
+
type: FileMagnifyingGlassIcon,
|
|
11287
|
+
selector: "file-magnifying-glass, file-magnifying-glass-icon, FileMagnifyingGlass, FileMagnifyingGlassIcon",
|
|
11288
|
+
inputs: ["class"]
|
|
11289
|
+
},
|
|
11290
|
+
{
|
|
11291
|
+
kind: "component",
|
|
11292
|
+
type: ListPanelEmptyComponent,
|
|
11293
|
+
selector: "list-panel-empty, ListPanelEmpty",
|
|
11294
|
+
inputs: ["color"]
|
|
11295
|
+
},
|
|
11296
|
+
{
|
|
11297
|
+
kind: "component",
|
|
11298
|
+
type: OccurrenceNavigatorComponent,
|
|
11299
|
+
selector: "occurrence-navigator, OccurrenceNavigator",
|
|
11300
|
+
inputs: [
|
|
11301
|
+
"active",
|
|
11302
|
+
"index",
|
|
11303
|
+
"total",
|
|
11304
|
+
"previousLabel",
|
|
11305
|
+
"nextLabel"
|
|
11306
|
+
],
|
|
11307
|
+
outputs: ["previous", "next"]
|
|
11308
|
+
},
|
|
11022
11309
|
{
|
|
11023
11310
|
kind: "pipe",
|
|
11024
11311
|
type: TranslocoPipe,
|
|
@@ -11041,20 +11328,23 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
11041
11328
|
ListComponent,
|
|
11042
11329
|
ListItemComponent,
|
|
11043
11330
|
ListItemTextComponent,
|
|
11044
|
-
ChevronLeftIcon,
|
|
11045
|
-
ChevronRightIcon,
|
|
11046
11331
|
KbdComponent,
|
|
11047
11332
|
KbdGroupComponent,
|
|
11048
11333
|
ArrowLeftIcon,
|
|
11049
|
-
ArrowRightIcon
|
|
11334
|
+
ArrowRightIcon,
|
|
11335
|
+
FileMagnifyingGlassIcon,
|
|
11336
|
+
ListPanelEmptyComponent,
|
|
11337
|
+
OccurrenceNavigatorComponent
|
|
11050
11338
|
],
|
|
11051
11339
|
template: `
|
|
11340
|
+
@if (state().hasContent()) {
|
|
11052
11341
|
<!-- Legend, once for the whole list — not repeated per row (aria-hidden: sighted-keyboard-user
|
|
11053
|
-
hint only, the buttons on each row already carry the real accessible names).
|
|
11342
|
+
hint only, the buttons on each row already carry the real accessible names). Hidden on a touch
|
|
11343
|
+
screen: there is no keyboard to press the keys on. -->
|
|
11054
11344
|
<div
|
|
11055
11345
|
data-name="shortcuts-legend"
|
|
11056
11346
|
aria-hidden="true"
|
|
11057
|
-
class="mb-1 flex items-center gap-1.5 px-2 py-1 text-[10px] text-(--font-info-base)">
|
|
11347
|
+
class="mb-1 flex items-center gap-1.5 px-2 py-1 text-[10px] text-(--font-info-base) pointer-coarse:hidden">
|
|
11058
11348
|
<KbdGroup class="text-[10px]">
|
|
11059
11349
|
<kbd size="xs" class="text-[10px]">Ctrl</kbd>
|
|
11060
11350
|
<kbd size="xs" class="text-[10px]"><arrow-left class="size-2.5" /></kbd>
|
|
@@ -11067,49 +11357,71 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
11067
11357
|
<span>{{ 'drawers.navigateOccurrences' | transloco }}</span>
|
|
11068
11358
|
</div>
|
|
11069
11359
|
|
|
11070
|
-
<List variant="list-menu" class="gap-
|
|
11360
|
+
<List variant="list-menu" class="gap-2" selectionMode="explicit">
|
|
11071
11361
|
@for (data of state().values(); track data.value) {
|
|
11072
11362
|
<ListItem
|
|
11073
11363
|
size="small"
|
|
11074
11364
|
[value]="data.value"
|
|
11075
|
-
class="select-none"
|
|
11076
|
-
(mouseenter)="
|
|
11077
|
-
(mouseleave)="
|
|
11365
|
+
class="select-none min-h-10 [&_.list-item-text]:text-(length:--size-lg)"
|
|
11366
|
+
(mouseenter)="row.enter(data.value)"
|
|
11367
|
+
(mouseleave)="row.leave()"
|
|
11368
|
+
(click)="row.activate(data.value)"
|
|
11078
11369
|
(focus)="focusedValue.set(data.value)"
|
|
11079
11370
|
(blur)="onBlur(data.value)"
|
|
11080
11371
|
(keydown.control.arrowleft)="onNavigateKey($event, data, 'prev')"
|
|
11081
11372
|
(keydown.control.arrowright)="onNavigateKey($event, data, 'next')">
|
|
11082
11373
|
<ListItemText>{{ data.display }}</ListItemText>
|
|
11083
11374
|
|
|
11084
|
-
<
|
|
11085
|
-
|
|
11086
|
-
|
|
11087
|
-
|
|
11088
|
-
|
|
11089
|
-
|
|
11090
|
-
|
|
11091
|
-
|
|
11092
|
-
|
|
11093
|
-
<chevron-right />
|
|
11094
|
-
</button>
|
|
11095
|
-
} @else {
|
|
11096
|
-
{{ data.count }}
|
|
11097
|
-
}
|
|
11098
|
-
</span>
|
|
11375
|
+
<occurrence-navigator
|
|
11376
|
+
class="ml-auto"
|
|
11377
|
+
[active]="isActive(data.value)"
|
|
11378
|
+
[index]="currentIndex(data)"
|
|
11379
|
+
[total]="data.count ?? 0"
|
|
11380
|
+
previousLabel="previous match"
|
|
11381
|
+
nextLabel="next match"
|
|
11382
|
+
(previous)="state().navigatePrev(data)"
|
|
11383
|
+
(next)="state().navigateNext(data)" />
|
|
11099
11384
|
</ListItem>
|
|
11100
11385
|
}
|
|
11101
11386
|
</List>
|
|
11387
|
+
} @else {
|
|
11388
|
+
<!-- With nothing to show it says why: no search yet, or no match. Two whole empty states rather than one with an @if inside:
|
|
11389
|
+
the hint is projected into its slot only when it is a direct child of <list-panel-empty> (NG8011 otherwise). -->
|
|
11390
|
+
@if (queryText()) {
|
|
11391
|
+
<list-panel-empty color="blue">
|
|
11392
|
+
<file-magnifying-glass-icon listPanelEmptyIcon />
|
|
11393
|
+
{{ 'drawers.noMatches' | transloco: { query: queryText() } }}
|
|
11394
|
+
<p listPanelEmptyHint>{{ 'drawers.noMatchesHint' | transloco }}</p>
|
|
11395
|
+
</list-panel-empty>
|
|
11396
|
+
} @else {
|
|
11397
|
+
<list-panel-empty color="blue">
|
|
11398
|
+
<file-magnifying-glass-icon listPanelEmptyIcon />
|
|
11399
|
+
{{ 'drawers.typeToSearch' | transloco }}
|
|
11400
|
+
<p listPanelEmptyHint>{{ 'drawers.typeToSearchHint' | transloco }}</p>
|
|
11401
|
+
</list-panel-empty>
|
|
11402
|
+
}
|
|
11403
|
+
}
|
|
11102
11404
|
`
|
|
11103
11405
|
}]
|
|
11104
11406
|
}],
|
|
11105
|
-
propDecorators: {
|
|
11106
|
-
|
|
11107
|
-
|
|
11108
|
-
|
|
11109
|
-
|
|
11110
|
-
|
|
11407
|
+
propDecorators: {
|
|
11408
|
+
state: [{
|
|
11409
|
+
type: i0.Input,
|
|
11410
|
+
args: [{
|
|
11411
|
+
isSignal: true,
|
|
11412
|
+
alias: "state",
|
|
11413
|
+
required: true
|
|
11414
|
+
}]
|
|
11415
|
+
}],
|
|
11416
|
+
queryText: [{
|
|
11417
|
+
type: i0.Input,
|
|
11418
|
+
args: [{
|
|
11419
|
+
isSignal: true,
|
|
11420
|
+
alias: "queryText",
|
|
11421
|
+
required: false
|
|
11422
|
+
}]
|
|
11111
11423
|
}]
|
|
11112
|
-
}
|
|
11424
|
+
}
|
|
11113
11425
|
});
|
|
11114
11426
|
var ArticleSimilarDocumentsTabPanel = class ArticleSimilarDocumentsTabPanel {
|
|
11115
11427
|
article = input.required(...ngDevMode ? [{ debugName: "article" }] : /* istanbul ignore next */ []);
|
|
@@ -11156,6 +11468,7 @@ var ArticleSimilarDocumentsTabPanel = class ArticleSimilarDocumentsTabPanel {
|
|
|
11156
11468
|
outputs: { selected: "selected" },
|
|
11157
11469
|
ngImport: i0,
|
|
11158
11470
|
template: `
|
|
11471
|
+
@if (state().hasContent()) {
|
|
11159
11472
|
<List variant="list-navigation" size="small" class="gap-1" selectionMode="explicit">
|
|
11160
11473
|
@for (document of state().similarDocuments(); track document.id) {
|
|
11161
11474
|
<ListItem
|
|
@@ -11173,6 +11486,14 @@ var ArticleSimilarDocumentsTabPanel = class ArticleSimilarDocumentsTabPanel {
|
|
|
11173
11486
|
</ListItem>
|
|
11174
11487
|
}
|
|
11175
11488
|
</List>
|
|
11489
|
+
} @else {
|
|
11490
|
+
<!-- Not reached through the tabs (they mount this panel once there is a document), but a host may mount it on its own. -->
|
|
11491
|
+
<list-panel-empty color="indigo">
|
|
11492
|
+
<files-icon listPanelEmptyIcon />
|
|
11493
|
+
{{ 'drawers.noSimilarDocuments' | transloco }}
|
|
11494
|
+
<p listPanelEmptyHint>{{ 'drawers.noSimilarDocumentsHint' | transloco }}</p>
|
|
11495
|
+
</list-panel-empty>
|
|
11496
|
+
}
|
|
11176
11497
|
`,
|
|
11177
11498
|
isInline: true,
|
|
11178
11499
|
dependencies: [
|
|
@@ -11182,6 +11503,12 @@ var ArticleSimilarDocumentsTabPanel = class ArticleSimilarDocumentsTabPanel {
|
|
|
11182
11503
|
selector: "source, Source",
|
|
11183
11504
|
inputs: ["collection", "connector"]
|
|
11184
11505
|
},
|
|
11506
|
+
{
|
|
11507
|
+
kind: "component",
|
|
11508
|
+
type: FilesIcon,
|
|
11509
|
+
selector: "files, files-icon, Files, FilesIcon",
|
|
11510
|
+
inputs: ["class"]
|
|
11511
|
+
},
|
|
11185
11512
|
{
|
|
11186
11513
|
kind: "directive",
|
|
11187
11514
|
type: ListComponent,
|
|
@@ -11224,6 +11551,12 @@ var ArticleSimilarDocumentsTabPanel = class ArticleSimilarDocumentsTabPanel {
|
|
|
11224
11551
|
inputs: ["class", "subtext"],
|
|
11225
11552
|
exportAs: ["listItemText"]
|
|
11226
11553
|
},
|
|
11554
|
+
{
|
|
11555
|
+
kind: "component",
|
|
11556
|
+
type: ListPanelEmptyComponent,
|
|
11557
|
+
selector: "list-panel-empty, ListPanelEmpty",
|
|
11558
|
+
inputs: ["color"]
|
|
11559
|
+
},
|
|
11227
11560
|
{
|
|
11228
11561
|
kind: "directive",
|
|
11229
11562
|
type: SelectArticleDirective,
|
|
@@ -11234,6 +11567,11 @@ var ArticleSimilarDocumentsTabPanel = class ArticleSimilarDocumentsTabPanel {
|
|
|
11234
11567
|
"redirectUrl"
|
|
11235
11568
|
],
|
|
11236
11569
|
outputs: ["selected"]
|
|
11570
|
+
},
|
|
11571
|
+
{
|
|
11572
|
+
kind: "pipe",
|
|
11573
|
+
type: TranslocoPipe,
|
|
11574
|
+
name: "transloco"
|
|
11237
11575
|
}
|
|
11238
11576
|
]
|
|
11239
11577
|
});
|
|
@@ -11248,15 +11586,19 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
11248
11586
|
args: [{
|
|
11249
11587
|
selector: "article-similar-documents-tab-panel, ArticleSimilarDocumentsTabPanel, articlesimilardocumentstabpanel",
|
|
11250
11588
|
imports: [
|
|
11589
|
+
TranslocoPipe,
|
|
11251
11590
|
SourceComponent,
|
|
11591
|
+
FilesIcon,
|
|
11252
11592
|
ListComponent,
|
|
11253
11593
|
ListItemComponent,
|
|
11254
11594
|
ListItemLeftDirective,
|
|
11255
11595
|
ListItemAvatarDirective,
|
|
11256
11596
|
ListItemTextComponent,
|
|
11597
|
+
ListPanelEmptyComponent,
|
|
11257
11598
|
SelectArticleDirective
|
|
11258
11599
|
],
|
|
11259
11600
|
template: `
|
|
11601
|
+
@if (state().hasContent()) {
|
|
11260
11602
|
<List variant="list-navigation" size="small" class="gap-1" selectionMode="explicit">
|
|
11261
11603
|
@for (document of state().similarDocuments(); track document.id) {
|
|
11262
11604
|
<ListItem
|
|
@@ -11274,6 +11616,14 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
11274
11616
|
</ListItem>
|
|
11275
11617
|
}
|
|
11276
11618
|
</List>
|
|
11619
|
+
} @else {
|
|
11620
|
+
<!-- Not reached through the tabs (they mount this panel once there is a document), but a host may mount it on its own. -->
|
|
11621
|
+
<list-panel-empty color="indigo">
|
|
11622
|
+
<files-icon listPanelEmptyIcon />
|
|
11623
|
+
{{ 'drawers.noSimilarDocuments' | transloco }}
|
|
11624
|
+
<p listPanelEmptyHint>{{ 'drawers.noSimilarDocumentsHint' | transloco }}</p>
|
|
11625
|
+
</list-panel-empty>
|
|
11626
|
+
}
|
|
11277
11627
|
`
|
|
11278
11628
|
}]
|
|
11279
11629
|
}],
|
|
@@ -11398,7 +11748,7 @@ var AdvancedSearchTabs = class AdvancedSearchTabs {
|
|
|
11398
11748
|
host: { classAttribute: "flex min-h-0 flex-col overflow-x-hidden text-(length:--size-xs)" },
|
|
11399
11749
|
providers: [provideTranslocoScope("drawers")],
|
|
11400
11750
|
ngImport: i0,
|
|
11401
|
-
template: "@if (article()) {\n @if (searchBar()) {\n <section class=\"mx-3 mt-3 mb-3 flex shrink-0 flex-col gap-4\">\n <!-- Enter submits through (keydown.enter), the send button through (search). No debounce: same \"submit on\n Enter/send, not on every keystroke\" contract as the previous input-group implementation. -->\n <SearchBar\n size=\"small\"\n [placeholder]=\"'drawers.inputPlaceholder' | transloco\"\n [aria-label]=\"'drawers.inputPlaceholder' | transloco\"\n [(value)]=\"queryText\"\n (keydown.enter)=\"executeSearch()\"\n (search)=\"executeSearch()\" />\n </section>\n }\n\n <!-- flex-1/min-h-0: this is what lets the panel content below be the only thing that scrolls —\n the search bar/tab list above are shrink-0, never pushed off or scrolled themselves. -->\n <Tabs class=\"flex min-h-0 flex-1 flex-col\">\n <!-- noTruncate skips the default `truncate` branch (whose `has-[span]:[&>span]:truncate` would\n otherwise force this span back to single-line ellipsis, defeating the wrap below); overriding\n its own `min-w-fit` with `min-w-[auto]` restores the flex item's normal \"automatic minimum\n size\" instead — for content that can wrap, that floor is the longest word (min-content), not\n the full unwrapped label (max-content, what `fit-content` resolves to here with nothing\n constraining the available space). A single-word label has nothing to wrap, so min-content ==\n max-content and it can't be squeezed below its own readable width — the flex-shrink deficit\n falls on the multi-word labels that actually have room to give, which wrap onto the span's\n line-clamp-2 instead of being cut with an ellipsis (appropriate for a single-line pill, not\n for a tab bar where \"Similar documents\"/\"Match locations\" routinely don't fit on one line).\n h-9 (fixed, not h-auto) + items-center on every tab keeps the row visually even whether a\n given label wraps onto 2 lines or stays on 1. No horizontal scroll/truncation fallback here on\n purpose: with all five tabs the row wraps its labels in the 360px panel of the preview's find\n field (see preview-actions.ts) rather than cutting them with an ellipsis. -->\n <TabList\n class=\"mx-3 shrink-0\"\n [class.mt-3]=\"!searchBar()\"\n variant=\"inner\"\n [selectedTab]=\"selectedTab()\"\n (selectedTabChange)=\"activeTab.set($event)\">\n <Tab\n variant=\"secondary\"\n size=\"sm\"\n value=\"snippet\"\n noTruncate\n class=\"h-9 min-w-[auto] items-center text-(length:--size-sm)\">\n <span\n class=\"line-clamp-2 text-center leading-tight whitespace-normal\"\n >{{ \"drawers.snippetTab\" | transloco }}</span\n >\n </Tab>\n <Tab\n variant=\"secondary\"\n size=\"sm\"\n value=\"extracts\"\n noTruncate\n class=\"h-9 min-w-[auto] items-center text-(length:--size-sm)\">\n <span\n class=\"line-clamp-2 text-center leading-tight whitespace-normal\"\n >{{ \"extracts\" | transloco }}</span\n >\n </Tab>\n @if (entities.hasContent()) {\n <Tab\n variant=\"secondary\"\n size=\"sm\"\n value=\"entities\"\n noTruncate\n class=\"h-9 min-w-[auto] items-center text-(length:--size-sm)\">\n <span\n class=\"line-clamp-2 text-center leading-tight whitespace-normal\"\n >{{ \"drawers.entitiesTab\" | transloco }}</span\n >\n </Tab>\n }\n @if (labels.hasContent()) {\n <Tab\n variant=\"secondary\"\n size=\"sm\"\n value=\"labels\"\n noTruncate\n class=\"h-9 min-w-[auto] items-center text-(length:--size-sm)\">\n <span\n class=\"line-clamp-2 text-center leading-tight whitespace-normal\"\n >{{ \"labels\" | transloco }}</span\n >\n </Tab>\n }\n @if (similar.hasContent()) {\n <Tab\n variant=\"secondary\"\n size=\"sm\"\n value=\"similar\"\n noTruncate\n class=\"h-9 min-w-[auto] items-center text-(length:--size-sm)\">\n <span\n class=\"line-clamp-2 text-center leading-tight whitespace-normal\"\n >{{ \"drawers.similarDocuments\" | transloco }}</span\n >\n </Tab>\n }\n </TabList>\n\n <!-- The single scroll container: each TabPanel is `absolute inset-0` (only the active one\n visible, per TabPanelComponent's own `[class.hidden]`), so they all share this one\n min-h-0/flex-1/overflow-y-auto region instead of the whole component scrolling. -->\n <div class=\"relative m-3 min-h-0 flex-1 scrollbar-thin overflow-y-auto\">\n <!-- Every TabPanel below cedes its own tab stop to its content (TabPanelComponent's `tabIndex`\n doc, ui/tabs/tabs.ts): the APG makes a tabpanel focusable only when it holds no focusable\n child, otherwise Tab from the tab lands on the empty container instead of the first row.\n `extracts` is always rendered (own empty/loading states, per the class doc above) so its\n tabindex is computed from whether it currently has any focusable row;\n `entities`/`labels`/`similar` are only ever mounted via `hasContent()`,\n which — per their `injectXState` — guarantees at least one row, so a static -1 is safe there. -->\n <!-- Always there, like Extracts: with nothing to show it says why — no search yet, or no match. The\n panel keeps its tab stop while it is empty (an empty tabpanel is the only thing Tab can reach\n from its tab), and cedes it to its first row as soon as there is one. -->\n <TabPanel\n value=\"snippet\"\n [tabindex]=\"matchLocations.hasContent() ? -1 : 0\"\n class=\"absolute inset-0\">\n
|
|
11751
|
+
template: "@if (article()) {\n @if (searchBar()) {\n <section class=\"mx-3 mt-3 mb-3 flex shrink-0 flex-col gap-4\">\n <!-- Enter submits through (keydown.enter), the send button through (search). No debounce: same \"submit on\n Enter/send, not on every keystroke\" contract as the previous input-group implementation. -->\n <SearchBar\n size=\"small\"\n [placeholder]=\"'drawers.inputPlaceholder' | transloco\"\n [aria-label]=\"'drawers.inputPlaceholder' | transloco\"\n [(value)]=\"queryText\"\n (keydown.enter)=\"executeSearch()\"\n (search)=\"executeSearch()\" />\n </section>\n }\n\n <!-- flex-1/min-h-0: this is what lets the panel content below be the only thing that scrolls —\n the search bar/tab list above are shrink-0, never pushed off or scrolled themselves. -->\n <Tabs class=\"flex min-h-0 flex-1 flex-col\">\n <!-- noTruncate skips the default `truncate` branch (whose `has-[span]:[&>span]:truncate` would\n otherwise force this span back to single-line ellipsis, defeating the wrap below); overriding\n its own `min-w-fit` with `min-w-[auto]` restores the flex item's normal \"automatic minimum\n size\" instead — for content that can wrap, that floor is the longest word (min-content), not\n the full unwrapped label (max-content, what `fit-content` resolves to here with nothing\n constraining the available space). A single-word label has nothing to wrap, so min-content ==\n max-content and it can't be squeezed below its own readable width — the flex-shrink deficit\n falls on the multi-word labels that actually have room to give, which wrap onto the span's\n line-clamp-2 instead of being cut with an ellipsis (appropriate for a single-line pill, not\n for a tab bar where \"Similar documents\"/\"Match locations\" routinely don't fit on one line).\n h-9 (fixed, not h-auto) + items-center on every tab keeps the row visually even whether a\n given label wraps onto 2 lines or stays on 1. No horizontal scroll/truncation fallback here on\n purpose: with all five tabs the row wraps its labels in the 360px panel of the preview's find\n field (see preview-actions.ts) rather than cutting them with an ellipsis. -->\n <TabList\n class=\"mx-3 shrink-0\"\n [class.mt-3]=\"!searchBar()\"\n variant=\"inner\"\n [selectedTab]=\"selectedTab()\"\n (selectedTabChange)=\"activeTab.set($event)\">\n <Tab\n variant=\"secondary\"\n size=\"sm\"\n value=\"snippet\"\n noTruncate\n class=\"h-9 min-w-[auto] items-center text-(length:--size-sm)\">\n <span\n class=\"line-clamp-2 text-center leading-tight whitespace-normal\"\n >{{ \"drawers.snippetTab\" | transloco }}</span\n >\n </Tab>\n <Tab\n variant=\"secondary\"\n size=\"sm\"\n value=\"extracts\"\n noTruncate\n class=\"h-9 min-w-[auto] items-center text-(length:--size-sm)\">\n <span\n class=\"line-clamp-2 text-center leading-tight whitespace-normal\"\n >{{ \"extracts\" | transloco }}</span\n >\n </Tab>\n @if (entities.hasContent()) {\n <Tab\n variant=\"secondary\"\n size=\"sm\"\n value=\"entities\"\n noTruncate\n class=\"h-9 min-w-[auto] items-center text-(length:--size-sm)\">\n <span\n class=\"line-clamp-2 text-center leading-tight whitespace-normal\"\n >{{ \"drawers.entitiesTab\" | transloco }}</span\n >\n </Tab>\n }\n @if (labels.hasContent()) {\n <Tab\n variant=\"secondary\"\n size=\"sm\"\n value=\"labels\"\n noTruncate\n class=\"h-9 min-w-[auto] items-center text-(length:--size-sm)\">\n <span\n class=\"line-clamp-2 text-center leading-tight whitespace-normal\"\n >{{ \"labels\" | transloco }}</span\n >\n </Tab>\n }\n @if (similar.hasContent()) {\n <Tab\n variant=\"secondary\"\n size=\"sm\"\n value=\"similar\"\n noTruncate\n class=\"h-9 min-w-[auto] items-center text-(length:--size-sm)\">\n <span\n class=\"line-clamp-2 text-center leading-tight whitespace-normal\"\n >{{ \"drawers.similarDocuments\" | transloco }}</span\n >\n </Tab>\n }\n </TabList>\n\n <!-- The single scroll container: each TabPanel is `absolute inset-0` (only the active one\n visible, per TabPanelComponent's own `[class.hidden]`), so they all share this one\n min-h-0/flex-1/overflow-y-auto region instead of the whole component scrolling. -->\n <div class=\"relative m-3 min-h-0 flex-1 scrollbar-thin overflow-y-auto\">\n <!-- Every TabPanel below cedes its own tab stop to its content (TabPanelComponent's `tabIndex`\n doc, ui/tabs/tabs.ts): the APG makes a tabpanel focusable only when it holds no focusable\n child, otherwise Tab from the tab lands on the empty container instead of the first row.\n `extracts` is always rendered (own empty/loading states, per the class doc above) so its\n tabindex is computed from whether it currently has any focusable row;\n `entities`/`labels`/`similar` are only ever mounted via `hasContent()`,\n which — per their `injectXState` — guarantees at least one row, so a static -1 is safe there. -->\n <!-- Always there, like Extracts: with nothing to show it says why — no search yet, or no match. The\n panel keeps its tab stop while it is empty (an empty tabpanel is the only thing Tab can reach\n from its tab), and cedes it to its first row as soon as there is one. -->\n <TabPanel\n value=\"snippet\"\n [tabindex]=\"matchLocations.hasContent() ? -1 : 0\"\n class=\"absolute inset-0\">\n <article-match-locations-tab-panel\n [state]=\"matchLocations\"\n [queryText]=\"queryText()\" />\n </TabPanel>\n <TabPanel\n value=\"extracts\"\n [tabindex]=\"\n extracts.loading() || extracts.extracts().length === 0 ? 0 : -1\n \"\n class=\"absolute inset-0\">\n <article-extracts-tab-panel [state]=\"extracts\" />\n </TabPanel>\n @if (entities.hasContent()) {\n <TabPanel value=\"entities\" tabindex=\"-1\" class=\"absolute inset-0\">\n <article-entities-tab-panel [state]=\"entities\" />\n </TabPanel>\n }\n @if (labels.hasContent()) {\n <TabPanel value=\"labels\" tabindex=\"-1\" class=\"absolute inset-0\">\n <article-labels-tab-panel [article]=\"article()\" [state]=\"labels\" />\n </TabPanel>\n }\n @if (similar.hasContent()) {\n <TabPanel value=\"similar\" tabindex=\"-1\" class=\"absolute inset-0\">\n <article-similar-documents-tab-panel\n [article]=\"article()\"\n [strategy]=\"previewStrategy()\"\n [state]=\"similar\"\n (selected)=\"selected.emit($event)\" />\n </TabPanel>\n }\n </div>\n </Tabs>\n}\n",
|
|
11402
11752
|
dependencies: [
|
|
11403
11753
|
{
|
|
11404
11754
|
kind: "component",
|
|
@@ -11456,7 +11806,7 @@ var AdvancedSearchTabs = class AdvancedSearchTabs {
|
|
|
11456
11806
|
kind: "component",
|
|
11457
11807
|
type: ArticleMatchLocationsTabPanel,
|
|
11458
11808
|
selector: "article-match-locations-tab-panel, ArticleMatchLocationsTabPanel, articlematchlocationstabpanel",
|
|
11459
|
-
inputs: ["state"]
|
|
11809
|
+
inputs: ["state", "queryText"]
|
|
11460
11810
|
},
|
|
11461
11811
|
{
|
|
11462
11812
|
kind: "component",
|
|
@@ -11501,7 +11851,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
11501
11851
|
ArticleMatchLocationsTabPanel,
|
|
11502
11852
|
ArticleSimilarDocumentsTabPanel
|
|
11503
11853
|
],
|
|
11504
|
-
template: "@if (article()) {\n @if (searchBar()) {\n <section class=\"mx-3 mt-3 mb-3 flex shrink-0 flex-col gap-4\">\n <!-- Enter submits through (keydown.enter), the send button through (search). No debounce: same \"submit on\n Enter/send, not on every keystroke\" contract as the previous input-group implementation. -->\n <SearchBar\n size=\"small\"\n [placeholder]=\"'drawers.inputPlaceholder' | transloco\"\n [aria-label]=\"'drawers.inputPlaceholder' | transloco\"\n [(value)]=\"queryText\"\n (keydown.enter)=\"executeSearch()\"\n (search)=\"executeSearch()\" />\n </section>\n }\n\n <!-- flex-1/min-h-0: this is what lets the panel content below be the only thing that scrolls —\n the search bar/tab list above are shrink-0, never pushed off or scrolled themselves. -->\n <Tabs class=\"flex min-h-0 flex-1 flex-col\">\n <!-- noTruncate skips the default `truncate` branch (whose `has-[span]:[&>span]:truncate` would\n otherwise force this span back to single-line ellipsis, defeating the wrap below); overriding\n its own `min-w-fit` with `min-w-[auto]` restores the flex item's normal \"automatic minimum\n size\" instead — for content that can wrap, that floor is the longest word (min-content), not\n the full unwrapped label (max-content, what `fit-content` resolves to here with nothing\n constraining the available space). A single-word label has nothing to wrap, so min-content ==\n max-content and it can't be squeezed below its own readable width — the flex-shrink deficit\n falls on the multi-word labels that actually have room to give, which wrap onto the span's\n line-clamp-2 instead of being cut with an ellipsis (appropriate for a single-line pill, not\n for a tab bar where \"Similar documents\"/\"Match locations\" routinely don't fit on one line).\n h-9 (fixed, not h-auto) + items-center on every tab keeps the row visually even whether a\n given label wraps onto 2 lines or stays on 1. No horizontal scroll/truncation fallback here on\n purpose: with all five tabs the row wraps its labels in the 360px panel of the preview's find\n field (see preview-actions.ts) rather than cutting them with an ellipsis. -->\n <TabList\n class=\"mx-3 shrink-0\"\n [class.mt-3]=\"!searchBar()\"\n variant=\"inner\"\n [selectedTab]=\"selectedTab()\"\n (selectedTabChange)=\"activeTab.set($event)\">\n <Tab\n variant=\"secondary\"\n size=\"sm\"\n value=\"snippet\"\n noTruncate\n class=\"h-9 min-w-[auto] items-center text-(length:--size-sm)\">\n <span\n class=\"line-clamp-2 text-center leading-tight whitespace-normal\"\n >{{ \"drawers.snippetTab\" | transloco }}</span\n >\n </Tab>\n <Tab\n variant=\"secondary\"\n size=\"sm\"\n value=\"extracts\"\n noTruncate\n class=\"h-9 min-w-[auto] items-center text-(length:--size-sm)\">\n <span\n class=\"line-clamp-2 text-center leading-tight whitespace-normal\"\n >{{ \"extracts\" | transloco }}</span\n >\n </Tab>\n @if (entities.hasContent()) {\n <Tab\n variant=\"secondary\"\n size=\"sm\"\n value=\"entities\"\n noTruncate\n class=\"h-9 min-w-[auto] items-center text-(length:--size-sm)\">\n <span\n class=\"line-clamp-2 text-center leading-tight whitespace-normal\"\n >{{ \"drawers.entitiesTab\" | transloco }}</span\n >\n </Tab>\n }\n @if (labels.hasContent()) {\n <Tab\n variant=\"secondary\"\n size=\"sm\"\n value=\"labels\"\n noTruncate\n class=\"h-9 min-w-[auto] items-center text-(length:--size-sm)\">\n <span\n class=\"line-clamp-2 text-center leading-tight whitespace-normal\"\n >{{ \"labels\" | transloco }}</span\n >\n </Tab>\n }\n @if (similar.hasContent()) {\n <Tab\n variant=\"secondary\"\n size=\"sm\"\n value=\"similar\"\n noTruncate\n class=\"h-9 min-w-[auto] items-center text-(length:--size-sm)\">\n <span\n class=\"line-clamp-2 text-center leading-tight whitespace-normal\"\n >{{ \"drawers.similarDocuments\" | transloco }}</span\n >\n </Tab>\n }\n </TabList>\n\n <!-- The single scroll container: each TabPanel is `absolute inset-0` (only the active one\n visible, per TabPanelComponent's own `[class.hidden]`), so they all share this one\n min-h-0/flex-1/overflow-y-auto region instead of the whole component scrolling. -->\n <div class=\"relative m-3 min-h-0 flex-1 scrollbar-thin overflow-y-auto\">\n <!-- Every TabPanel below cedes its own tab stop to its content (TabPanelComponent's `tabIndex`\n doc, ui/tabs/tabs.ts): the APG makes a tabpanel focusable only when it holds no focusable\n child, otherwise Tab from the tab lands on the empty container instead of the first row.\n `extracts` is always rendered (own empty/loading states, per the class doc above) so its\n tabindex is computed from whether it currently has any focusable row;\n `entities`/`labels`/`similar` are only ever mounted via `hasContent()`,\n which — per their `injectXState` — guarantees at least one row, so a static -1 is safe there. -->\n <!-- Always there, like Extracts: with nothing to show it says why — no search yet, or no match. The\n panel keeps its tab stop while it is empty (an empty tabpanel is the only thing Tab can reach\n from its tab), and cedes it to its first row as soon as there is one. -->\n <TabPanel\n value=\"snippet\"\n [tabindex]=\"matchLocations.hasContent() ? -1 : 0\"\n class=\"absolute inset-0\">\n
|
|
11854
|
+
template: "@if (article()) {\n @if (searchBar()) {\n <section class=\"mx-3 mt-3 mb-3 flex shrink-0 flex-col gap-4\">\n <!-- Enter submits through (keydown.enter), the send button through (search). No debounce: same \"submit on\n Enter/send, not on every keystroke\" contract as the previous input-group implementation. -->\n <SearchBar\n size=\"small\"\n [placeholder]=\"'drawers.inputPlaceholder' | transloco\"\n [aria-label]=\"'drawers.inputPlaceholder' | transloco\"\n [(value)]=\"queryText\"\n (keydown.enter)=\"executeSearch()\"\n (search)=\"executeSearch()\" />\n </section>\n }\n\n <!-- flex-1/min-h-0: this is what lets the panel content below be the only thing that scrolls —\n the search bar/tab list above are shrink-0, never pushed off or scrolled themselves. -->\n <Tabs class=\"flex min-h-0 flex-1 flex-col\">\n <!-- noTruncate skips the default `truncate` branch (whose `has-[span]:[&>span]:truncate` would\n otherwise force this span back to single-line ellipsis, defeating the wrap below); overriding\n its own `min-w-fit` with `min-w-[auto]` restores the flex item's normal \"automatic minimum\n size\" instead — for content that can wrap, that floor is the longest word (min-content), not\n the full unwrapped label (max-content, what `fit-content` resolves to here with nothing\n constraining the available space). A single-word label has nothing to wrap, so min-content ==\n max-content and it can't be squeezed below its own readable width — the flex-shrink deficit\n falls on the multi-word labels that actually have room to give, which wrap onto the span's\n line-clamp-2 instead of being cut with an ellipsis (appropriate for a single-line pill, not\n for a tab bar where \"Similar documents\"/\"Match locations\" routinely don't fit on one line).\n h-9 (fixed, not h-auto) + items-center on every tab keeps the row visually even whether a\n given label wraps onto 2 lines or stays on 1. No horizontal scroll/truncation fallback here on\n purpose: with all five tabs the row wraps its labels in the 360px panel of the preview's find\n field (see preview-actions.ts) rather than cutting them with an ellipsis. -->\n <TabList\n class=\"mx-3 shrink-0\"\n [class.mt-3]=\"!searchBar()\"\n variant=\"inner\"\n [selectedTab]=\"selectedTab()\"\n (selectedTabChange)=\"activeTab.set($event)\">\n <Tab\n variant=\"secondary\"\n size=\"sm\"\n value=\"snippet\"\n noTruncate\n class=\"h-9 min-w-[auto] items-center text-(length:--size-sm)\">\n <span\n class=\"line-clamp-2 text-center leading-tight whitespace-normal\"\n >{{ \"drawers.snippetTab\" | transloco }}</span\n >\n </Tab>\n <Tab\n variant=\"secondary\"\n size=\"sm\"\n value=\"extracts\"\n noTruncate\n class=\"h-9 min-w-[auto] items-center text-(length:--size-sm)\">\n <span\n class=\"line-clamp-2 text-center leading-tight whitespace-normal\"\n >{{ \"extracts\" | transloco }}</span\n >\n </Tab>\n @if (entities.hasContent()) {\n <Tab\n variant=\"secondary\"\n size=\"sm\"\n value=\"entities\"\n noTruncate\n class=\"h-9 min-w-[auto] items-center text-(length:--size-sm)\">\n <span\n class=\"line-clamp-2 text-center leading-tight whitespace-normal\"\n >{{ \"drawers.entitiesTab\" | transloco }}</span\n >\n </Tab>\n }\n @if (labels.hasContent()) {\n <Tab\n variant=\"secondary\"\n size=\"sm\"\n value=\"labels\"\n noTruncate\n class=\"h-9 min-w-[auto] items-center text-(length:--size-sm)\">\n <span\n class=\"line-clamp-2 text-center leading-tight whitespace-normal\"\n >{{ \"labels\" | transloco }}</span\n >\n </Tab>\n }\n @if (similar.hasContent()) {\n <Tab\n variant=\"secondary\"\n size=\"sm\"\n value=\"similar\"\n noTruncate\n class=\"h-9 min-w-[auto] items-center text-(length:--size-sm)\">\n <span\n class=\"line-clamp-2 text-center leading-tight whitespace-normal\"\n >{{ \"drawers.similarDocuments\" | transloco }}</span\n >\n </Tab>\n }\n </TabList>\n\n <!-- The single scroll container: each TabPanel is `absolute inset-0` (only the active one\n visible, per TabPanelComponent's own `[class.hidden]`), so they all share this one\n min-h-0/flex-1/overflow-y-auto region instead of the whole component scrolling. -->\n <div class=\"relative m-3 min-h-0 flex-1 scrollbar-thin overflow-y-auto\">\n <!-- Every TabPanel below cedes its own tab stop to its content (TabPanelComponent's `tabIndex`\n doc, ui/tabs/tabs.ts): the APG makes a tabpanel focusable only when it holds no focusable\n child, otherwise Tab from the tab lands on the empty container instead of the first row.\n `extracts` is always rendered (own empty/loading states, per the class doc above) so its\n tabindex is computed from whether it currently has any focusable row;\n `entities`/`labels`/`similar` are only ever mounted via `hasContent()`,\n which — per their `injectXState` — guarantees at least one row, so a static -1 is safe there. -->\n <!-- Always there, like Extracts: with nothing to show it says why — no search yet, or no match. The\n panel keeps its tab stop while it is empty (an empty tabpanel is the only thing Tab can reach\n from its tab), and cedes it to its first row as soon as there is one. -->\n <TabPanel\n value=\"snippet\"\n [tabindex]=\"matchLocations.hasContent() ? -1 : 0\"\n class=\"absolute inset-0\">\n <article-match-locations-tab-panel\n [state]=\"matchLocations\"\n [queryText]=\"queryText()\" />\n </TabPanel>\n <TabPanel\n value=\"extracts\"\n [tabindex]=\"\n extracts.loading() || extracts.extracts().length === 0 ? 0 : -1\n \"\n class=\"absolute inset-0\">\n <article-extracts-tab-panel [state]=\"extracts\" />\n </TabPanel>\n @if (entities.hasContent()) {\n <TabPanel value=\"entities\" tabindex=\"-1\" class=\"absolute inset-0\">\n <article-entities-tab-panel [state]=\"entities\" />\n </TabPanel>\n }\n @if (labels.hasContent()) {\n <TabPanel value=\"labels\" tabindex=\"-1\" class=\"absolute inset-0\">\n <article-labels-tab-panel [article]=\"article()\" [state]=\"labels\" />\n </TabPanel>\n }\n @if (similar.hasContent()) {\n <TabPanel value=\"similar\" tabindex=\"-1\" class=\"absolute inset-0\">\n <article-similar-documents-tab-panel\n [article]=\"article()\"\n [strategy]=\"previewStrategy()\"\n [state]=\"similar\"\n (selected)=\"selected.emit($event)\" />\n </TabPanel>\n }\n </div>\n </Tabs>\n}\n"
|
|
11505
11855
|
}]
|
|
11506
11856
|
}],
|
|
11507
11857
|
propDecorators: {
|
|
@@ -11695,7 +12045,9 @@ var ArticleHighlights = class ArticleHighlights {
|
|
|
11695
12045
|
<!-- A quiet row: the round mark is the category's colour when it is on, a ring when it is off. The arrows
|
|
11696
12046
|
take the place of the count while the row is hovered or holds the focus: six sets of arrows at rest were
|
|
11697
12047
|
the loudest thing in the panel. Nothing is kept free for them, so a name is cut (and shown in full in a
|
|
11698
|
-
tooltip) only when it does not fit, with the arrows when they are there.
|
|
12048
|
+
tooltip) only when it does not fit, with the arrows when they are there. On a touch screen nothing hovers,
|
|
12049
|
+
and a browser such as Safari does not focus a button at a tap: the arrows of every category that is on are
|
|
12050
|
+
there all the time (a category that is off has nothing to step through). -->
|
|
11699
12051
|
<li
|
|
11700
12052
|
role="listitem"
|
|
11701
12053
|
class="group/row relative flex h-10 items-center rounded-(--radius-md) select-none focus-within:bg-(--bg-primary-lightest) hover:bg-(--bg-primary-lightest)">
|
|
@@ -11703,11 +12055,11 @@ var ArticleHighlights = class ArticleHighlights {
|
|
|
11703
12055
|
type="button"
|
|
11704
12056
|
variant="none"
|
|
11705
12057
|
size="md"
|
|
11706
|
-
class="h-
|
|
12058
|
+
class="h-10 min-w-0 flex-1 justify-start gap-(--spacing-xs) rounded-(--radius-sm) px-(--spacing-2xs) text-start text-(--font-neutral-muted) focus-visible:-outline-offset-2 aria-pressed:text-(--font-neutral-black)"
|
|
11707
12059
|
[attr.aria-pressed]="isActive(cat.key)"
|
|
11708
12060
|
(click)="toggleCategory(cat.key)">
|
|
11709
12061
|
<!-- The button wraps what it holds in its own label: the layout of the row is this span's. -->
|
|
11710
|
-
<span class="flex w-full min-w-0 items-center gap-(--spacing-xs) text-(length:--size-
|
|
12062
|
+
<span class="flex w-full min-w-0 items-center gap-(--spacing-xs) text-(length:--size-lg) leading-(--line-height-3) font-normal">
|
|
11711
12063
|
<span
|
|
11712
12064
|
class="inline-flex size-5 shrink-0 items-center justify-center rounded-full border-2 border-(--stroke-neutral-base)"
|
|
11713
12065
|
[style.background]="isActive(cat.key) ? cat.color : null"
|
|
@@ -11719,7 +12071,7 @@ var ArticleHighlights = class ArticleHighlights {
|
|
|
11719
12071
|
<span class="min-w-0 truncate" truncated #cut="truncated" [tooltip]="categoryName(cat)" [tooltipDisabled]="!cut.isTruncated()">{{ categoryName(cat) }}</span>
|
|
11720
12072
|
<span
|
|
11721
12073
|
class="ms-auto ps-(--spacing-xs) pe-(--spacing-2xs) text-(length:--size-sm) text-(--font-neutral-muted) tabular-nums"
|
|
11722
|
-
[class]="isActive(cat.key) ? 'group-focus-within/row:hidden group-hover/row:hidden' : ''"
|
|
12074
|
+
[class]="isActive(cat.key) ? 'group-focus-within/row:hidden group-hover/row:hidden pointer-coarse:hidden' : ''"
|
|
11723
12075
|
>{{ cat.count }}</span
|
|
11724
12076
|
>
|
|
11725
12077
|
</span>
|
|
@@ -11727,30 +12079,15 @@ var ArticleHighlights = class ArticleHighlights {
|
|
|
11727
12079
|
|
|
11728
12080
|
@if (isActive(cat.key)) {
|
|
11729
12081
|
<span
|
|
11730
|
-
class="hidden shrink-0 items-center
|
|
11731
|
-
<
|
|
11732
|
-
|
|
11733
|
-
|
|
11734
|
-
|
|
11735
|
-
|
|
11736
|
-
[
|
|
11737
|
-
|
|
11738
|
-
(
|
|
11739
|
-
<chevron-left />
|
|
11740
|
-
</button>
|
|
11741
|
-
|
|
11742
|
-
<span class="min-w-12 text-center">{{ cursorLabel(cat.key) }} / {{ cat.count }}</span>
|
|
11743
|
-
|
|
11744
|
-
<button
|
|
11745
|
-
type="button"
|
|
11746
|
-
variant="tertiary"
|
|
11747
|
-
scheme="neutral"
|
|
11748
|
-
size="sm"
|
|
11749
|
-
[iconOnly]="true"
|
|
11750
|
-
[attr.aria-label]="'drawers.nextOccurrence' | transloco: { label: categoryName(cat) }"
|
|
11751
|
-
(click)="gotoCat(cat.key, 1)">
|
|
11752
|
-
<chevron-right />
|
|
11753
|
-
</button>
|
|
12082
|
+
class="hidden shrink-0 items-center pe-(--spacing-3xs) ps-(--spacing-2xs) text-(length:--size-sm) text-(--font-neutral-muted) group-focus-within/row:flex group-hover/row:flex pointer-coarse:flex">
|
|
12083
|
+
<occurrence-navigator
|
|
12084
|
+
[active]="true"
|
|
12085
|
+
[index]="cursorLabel(cat.key)"
|
|
12086
|
+
[total]="cat.count"
|
|
12087
|
+
[previousLabel]="'drawers.previousOccurrence' | transloco: { label: categoryName(cat) }"
|
|
12088
|
+
[nextLabel]="'drawers.nextOccurrence' | transloco: { label: categoryName(cat) }"
|
|
12089
|
+
(previous)="gotoCat(cat.key, -1)"
|
|
12090
|
+
(next)="gotoCat(cat.key, 1)" />
|
|
11754
12091
|
</span>
|
|
11755
12092
|
}
|
|
11756
12093
|
</li>
|
|
@@ -11765,6 +12102,13 @@ var ArticleHighlights = class ArticleHighlights {
|
|
|
11765
12102
|
<span class="ms-auto" aria-live="polite">{{ "drawers.highlightsShown" | transloco: { shown: active().length, total: categories().length } }}</span>
|
|
11766
12103
|
</div>
|
|
11767
12104
|
</div>
|
|
12105
|
+
} @else {
|
|
12106
|
+
<!-- The empty-state rule of the list features: the feature's icon on a coloured tile, a title, one hint line. -->
|
|
12107
|
+
<list-panel-empty color="orange">
|
|
12108
|
+
<highlighter-icon listPanelEmptyIcon />
|
|
12109
|
+
{{ 'drawers.noHighlights' | transloco }}
|
|
12110
|
+
<p listPanelEmptyHint>{{ 'drawers.noHighlightsHint' | transloco }}</p>
|
|
12111
|
+
</list-panel-empty>
|
|
11768
12112
|
}
|
|
11769
12113
|
`,
|
|
11770
12114
|
isInline: true,
|
|
@@ -11789,15 +12133,28 @@ var ArticleHighlights = class ArticleHighlights {
|
|
|
11789
12133
|
},
|
|
11790
12134
|
{
|
|
11791
12135
|
kind: "component",
|
|
11792
|
-
type:
|
|
11793
|
-
selector: "
|
|
12136
|
+
type: HighlighterIcon,
|
|
12137
|
+
selector: "highlighter, highlighter-icon, Highlighter, HighlighterIcon",
|
|
11794
12138
|
inputs: ["class"]
|
|
11795
12139
|
},
|
|
11796
12140
|
{
|
|
11797
12141
|
kind: "component",
|
|
11798
|
-
type:
|
|
11799
|
-
selector: "
|
|
11800
|
-
inputs: ["
|
|
12142
|
+
type: ListPanelEmptyComponent,
|
|
12143
|
+
selector: "list-panel-empty, ListPanelEmpty",
|
|
12144
|
+
inputs: ["color"]
|
|
12145
|
+
},
|
|
12146
|
+
{
|
|
12147
|
+
kind: "component",
|
|
12148
|
+
type: OccurrenceNavigatorComponent,
|
|
12149
|
+
selector: "occurrence-navigator, OccurrenceNavigator",
|
|
12150
|
+
inputs: [
|
|
12151
|
+
"active",
|
|
12152
|
+
"index",
|
|
12153
|
+
"total",
|
|
12154
|
+
"previousLabel",
|
|
12155
|
+
"nextLabel"
|
|
12156
|
+
],
|
|
12157
|
+
outputs: ["previous", "next"]
|
|
11801
12158
|
},
|
|
11802
12159
|
{
|
|
11803
12160
|
kind: "component",
|
|
@@ -11870,8 +12227,9 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
11870
12227
|
TranslocoPipe,
|
|
11871
12228
|
ButtonComponent,
|
|
11872
12229
|
CheckIcon,
|
|
11873
|
-
|
|
11874
|
-
|
|
12230
|
+
HighlighterIcon,
|
|
12231
|
+
ListPanelEmptyComponent,
|
|
12232
|
+
OccurrenceNavigatorComponent,
|
|
11875
12233
|
SwitchComponent,
|
|
11876
12234
|
TooltipDirective,
|
|
11877
12235
|
TruncatedDirective
|
|
@@ -11885,7 +12243,9 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
11885
12243
|
<!-- A quiet row: the round mark is the category's colour when it is on, a ring when it is off. The arrows
|
|
11886
12244
|
take the place of the count while the row is hovered or holds the focus: six sets of arrows at rest were
|
|
11887
12245
|
the loudest thing in the panel. Nothing is kept free for them, so a name is cut (and shown in full in a
|
|
11888
|
-
tooltip) only when it does not fit, with the arrows when they are there.
|
|
12246
|
+
tooltip) only when it does not fit, with the arrows when they are there. On a touch screen nothing hovers,
|
|
12247
|
+
and a browser such as Safari does not focus a button at a tap: the arrows of every category that is on are
|
|
12248
|
+
there all the time (a category that is off has nothing to step through). -->
|
|
11889
12249
|
<li
|
|
11890
12250
|
role="listitem"
|
|
11891
12251
|
class="group/row relative flex h-10 items-center rounded-(--radius-md) select-none focus-within:bg-(--bg-primary-lightest) hover:bg-(--bg-primary-lightest)">
|
|
@@ -11893,11 +12253,11 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
11893
12253
|
type="button"
|
|
11894
12254
|
variant="none"
|
|
11895
12255
|
size="md"
|
|
11896
|
-
class="h-
|
|
12256
|
+
class="h-10 min-w-0 flex-1 justify-start gap-(--spacing-xs) rounded-(--radius-sm) px-(--spacing-2xs) text-start text-(--font-neutral-muted) focus-visible:-outline-offset-2 aria-pressed:text-(--font-neutral-black)"
|
|
11897
12257
|
[attr.aria-pressed]="isActive(cat.key)"
|
|
11898
12258
|
(click)="toggleCategory(cat.key)">
|
|
11899
12259
|
<!-- The button wraps what it holds in its own label: the layout of the row is this span's. -->
|
|
11900
|
-
<span class="flex w-full min-w-0 items-center gap-(--spacing-xs) text-(length:--size-
|
|
12260
|
+
<span class="flex w-full min-w-0 items-center gap-(--spacing-xs) text-(length:--size-lg) leading-(--line-height-3) font-normal">
|
|
11901
12261
|
<span
|
|
11902
12262
|
class="inline-flex size-5 shrink-0 items-center justify-center rounded-full border-2 border-(--stroke-neutral-base)"
|
|
11903
12263
|
[style.background]="isActive(cat.key) ? cat.color : null"
|
|
@@ -11909,7 +12269,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
11909
12269
|
<span class="min-w-0 truncate" truncated #cut="truncated" [tooltip]="categoryName(cat)" [tooltipDisabled]="!cut.isTruncated()">{{ categoryName(cat) }}</span>
|
|
11910
12270
|
<span
|
|
11911
12271
|
class="ms-auto ps-(--spacing-xs) pe-(--spacing-2xs) text-(length:--size-sm) text-(--font-neutral-muted) tabular-nums"
|
|
11912
|
-
[class]="isActive(cat.key) ? 'group-focus-within/row:hidden group-hover/row:hidden' : ''"
|
|
12272
|
+
[class]="isActive(cat.key) ? 'group-focus-within/row:hidden group-hover/row:hidden pointer-coarse:hidden' : ''"
|
|
11913
12273
|
>{{ cat.count }}</span
|
|
11914
12274
|
>
|
|
11915
12275
|
</span>
|
|
@@ -11917,30 +12277,15 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
11917
12277
|
|
|
11918
12278
|
@if (isActive(cat.key)) {
|
|
11919
12279
|
<span
|
|
11920
|
-
class="hidden shrink-0 items-center
|
|
11921
|
-
<
|
|
11922
|
-
|
|
11923
|
-
|
|
11924
|
-
|
|
11925
|
-
|
|
11926
|
-
[
|
|
11927
|
-
|
|
11928
|
-
(
|
|
11929
|
-
<chevron-left />
|
|
11930
|
-
</button>
|
|
11931
|
-
|
|
11932
|
-
<span class="min-w-12 text-center">{{ cursorLabel(cat.key) }} / {{ cat.count }}</span>
|
|
11933
|
-
|
|
11934
|
-
<button
|
|
11935
|
-
type="button"
|
|
11936
|
-
variant="tertiary"
|
|
11937
|
-
scheme="neutral"
|
|
11938
|
-
size="sm"
|
|
11939
|
-
[iconOnly]="true"
|
|
11940
|
-
[attr.aria-label]="'drawers.nextOccurrence' | transloco: { label: categoryName(cat) }"
|
|
11941
|
-
(click)="gotoCat(cat.key, 1)">
|
|
11942
|
-
<chevron-right />
|
|
11943
|
-
</button>
|
|
12280
|
+
class="hidden shrink-0 items-center pe-(--spacing-3xs) ps-(--spacing-2xs) text-(length:--size-sm) text-(--font-neutral-muted) group-focus-within/row:flex group-hover/row:flex pointer-coarse:flex">
|
|
12281
|
+
<occurrence-navigator
|
|
12282
|
+
[active]="true"
|
|
12283
|
+
[index]="cursorLabel(cat.key)"
|
|
12284
|
+
[total]="cat.count"
|
|
12285
|
+
[previousLabel]="'drawers.previousOccurrence' | transloco: { label: categoryName(cat) }"
|
|
12286
|
+
[nextLabel]="'drawers.nextOccurrence' | transloco: { label: categoryName(cat) }"
|
|
12287
|
+
(previous)="gotoCat(cat.key, -1)"
|
|
12288
|
+
(next)="gotoCat(cat.key, 1)" />
|
|
11944
12289
|
</span>
|
|
11945
12290
|
}
|
|
11946
12291
|
</li>
|
|
@@ -11955,6 +12300,13 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
11955
12300
|
<span class="ms-auto" aria-live="polite">{{ "drawers.highlightsShown" | transloco: { shown: active().length, total: categories().length } }}</span>
|
|
11956
12301
|
</div>
|
|
11957
12302
|
</div>
|
|
12303
|
+
} @else {
|
|
12304
|
+
<!-- The empty-state rule of the list features: the feature's icon on a coloured tile, a title, one hint line. -->
|
|
12305
|
+
<list-panel-empty color="orange">
|
|
12306
|
+
<highlighter-icon listPanelEmptyIcon />
|
|
12307
|
+
{{ 'drawers.noHighlights' | transloco }}
|
|
12308
|
+
<p listPanelEmptyHint>{{ 'drawers.noHighlightsHint' | transloco }}</p>
|
|
12309
|
+
</list-panel-empty>
|
|
11958
12310
|
}
|
|
11959
12311
|
`
|
|
11960
12312
|
}]
|
|
@@ -13163,6 +13515,154 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
13163
13515
|
}]
|
|
13164
13516
|
}
|
|
13165
13517
|
});
|
|
13518
|
+
function distance(touches) {
|
|
13519
|
+
return Math.hypot(touches[0].clientX - touches[1].clientX, touches[0].clientY - touches[1].clientY);
|
|
13520
|
+
}
|
|
13521
|
+
var PinchZoomDirective = class PinchZoomDirective {
|
|
13522
|
+
zoom = model.required({
|
|
13523
|
+
...ngDevMode ? { debugName: "zoom" } : /* istanbul ignore next */ {},
|
|
13524
|
+
alias: "pinchZoom"
|
|
13525
|
+
});
|
|
13526
|
+
min = input(.5, {
|
|
13527
|
+
...ngDevMode ? { debugName: "min" } : /* istanbul ignore next */ {},
|
|
13528
|
+
alias: "pinchZoomMin",
|
|
13529
|
+
transform: numberAttribute
|
|
13530
|
+
});
|
|
13531
|
+
max = input(2, {
|
|
13532
|
+
...ngDevMode ? { debugName: "max" } : /* istanbul ignore next */ {},
|
|
13533
|
+
alias: "pinchZoomMax",
|
|
13534
|
+
transform: numberAttribute
|
|
13535
|
+
});
|
|
13536
|
+
host = inject(ElementRef).nativeElement;
|
|
13537
|
+
injector = inject(Injector);
|
|
13538
|
+
pinch = null;
|
|
13539
|
+
constructor() {
|
|
13540
|
+
const start = (event) => {
|
|
13541
|
+
this.pinch = null;
|
|
13542
|
+
if (event.touches.length !== 2) return;
|
|
13543
|
+
const startDistance = distance(event.touches);
|
|
13544
|
+
if (startDistance >= 1) this.pinch = {
|
|
13545
|
+
startDistance,
|
|
13546
|
+
startZoom: this.zoom()
|
|
13547
|
+
};
|
|
13548
|
+
};
|
|
13549
|
+
const end = () => {
|
|
13550
|
+
this.pinch = null;
|
|
13551
|
+
};
|
|
13552
|
+
this.host.addEventListener("touchstart", start, { passive: true });
|
|
13553
|
+
this.host.addEventListener("touchmove", this.onMove, { passive: false });
|
|
13554
|
+
this.host.addEventListener("touchend", end, { passive: true });
|
|
13555
|
+
this.host.addEventListener("touchcancel", end, { passive: true });
|
|
13556
|
+
inject(DestroyRef).onDestroy(() => {
|
|
13557
|
+
this.host.removeEventListener("touchstart", start);
|
|
13558
|
+
this.host.removeEventListener("touchmove", this.onMove);
|
|
13559
|
+
this.host.removeEventListener("touchend", end);
|
|
13560
|
+
this.host.removeEventListener("touchcancel", end);
|
|
13561
|
+
});
|
|
13562
|
+
}
|
|
13563
|
+
onMove = (event) => {
|
|
13564
|
+
if (!this.pinch || event.touches.length !== 2) return;
|
|
13565
|
+
event.preventDefault();
|
|
13566
|
+
const previous = this.zoom();
|
|
13567
|
+
const next = Math.min(this.max(), Math.max(this.min(), this.pinch.startZoom * (distance(event.touches) / this.pinch.startDistance)));
|
|
13568
|
+
if (next === previous) return;
|
|
13569
|
+
const rect = this.host.getBoundingClientRect();
|
|
13570
|
+
const x = (event.touches[0].clientX + event.touches[1].clientX) / 2 - rect.left;
|
|
13571
|
+
const y = (event.touches[0].clientY + event.touches[1].clientY) / 2 - rect.top;
|
|
13572
|
+
const left = this.host.scrollLeft;
|
|
13573
|
+
const top = this.host.scrollTop;
|
|
13574
|
+
this.zoom.set(next);
|
|
13575
|
+
afterNextRender({ write: () => {
|
|
13576
|
+
const ratio = next / previous;
|
|
13577
|
+
this.host.scrollLeft = (left + x) * ratio - x;
|
|
13578
|
+
this.host.scrollTop = (top + y) * ratio - y;
|
|
13579
|
+
} }, { injector: this.injector });
|
|
13580
|
+
};
|
|
13581
|
+
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
13582
|
+
minVersion: "12.0.0",
|
|
13583
|
+
version: "22.2.1",
|
|
13584
|
+
ngImport: i0,
|
|
13585
|
+
type: PinchZoomDirective,
|
|
13586
|
+
deps: [],
|
|
13587
|
+
target: i0.ɵɵFactoryTarget.Directive
|
|
13588
|
+
});
|
|
13589
|
+
static ɵdir = i0.ɵɵngDeclareDirective({
|
|
13590
|
+
minVersion: "17.1.0",
|
|
13591
|
+
version: "22.2.1",
|
|
13592
|
+
type: PinchZoomDirective,
|
|
13593
|
+
isStandalone: true,
|
|
13594
|
+
selector: "[pinchZoom]",
|
|
13595
|
+
inputs: {
|
|
13596
|
+
zoom: {
|
|
13597
|
+
classPropertyName: "zoom",
|
|
13598
|
+
publicName: "pinchZoom",
|
|
13599
|
+
isSignal: true,
|
|
13600
|
+
isRequired: true,
|
|
13601
|
+
transformFunction: null
|
|
13602
|
+
},
|
|
13603
|
+
min: {
|
|
13604
|
+
classPropertyName: "min",
|
|
13605
|
+
publicName: "pinchZoomMin",
|
|
13606
|
+
isSignal: true,
|
|
13607
|
+
isRequired: false,
|
|
13608
|
+
transformFunction: null
|
|
13609
|
+
},
|
|
13610
|
+
max: {
|
|
13611
|
+
classPropertyName: "max",
|
|
13612
|
+
publicName: "pinchZoomMax",
|
|
13613
|
+
isSignal: true,
|
|
13614
|
+
isRequired: false,
|
|
13615
|
+
transformFunction: null
|
|
13616
|
+
}
|
|
13617
|
+
},
|
|
13618
|
+
outputs: { zoom: "pinchZoomChange" },
|
|
13619
|
+
host: { properties: { "style.touch-action": "'pan-x pan-y'" } },
|
|
13620
|
+
ngImport: i0
|
|
13621
|
+
});
|
|
13622
|
+
};
|
|
13623
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
13624
|
+
minVersion: "12.0.0",
|
|
13625
|
+
version: "22.2.1",
|
|
13626
|
+
ngImport: i0,
|
|
13627
|
+
type: PinchZoomDirective,
|
|
13628
|
+
decorators: [{
|
|
13629
|
+
type: Directive,
|
|
13630
|
+
args: [{
|
|
13631
|
+
selector: "[pinchZoom]",
|
|
13632
|
+
host: { "[style.touch-action]": "'pan-x pan-y'" }
|
|
13633
|
+
}]
|
|
13634
|
+
}],
|
|
13635
|
+
ctorParameters: () => [],
|
|
13636
|
+
propDecorators: {
|
|
13637
|
+
zoom: [{
|
|
13638
|
+
type: i0.Input,
|
|
13639
|
+
args: [{
|
|
13640
|
+
isSignal: true,
|
|
13641
|
+
alias: "pinchZoom",
|
|
13642
|
+
required: true
|
|
13643
|
+
}]
|
|
13644
|
+
}, {
|
|
13645
|
+
type: i0.Output,
|
|
13646
|
+
args: ["pinchZoomChange"]
|
|
13647
|
+
}],
|
|
13648
|
+
min: [{
|
|
13649
|
+
type: i0.Input,
|
|
13650
|
+
args: [{
|
|
13651
|
+
isSignal: true,
|
|
13652
|
+
alias: "pinchZoomMin",
|
|
13653
|
+
required: false
|
|
13654
|
+
}]
|
|
13655
|
+
}],
|
|
13656
|
+
max: [{
|
|
13657
|
+
type: i0.Input,
|
|
13658
|
+
args: [{
|
|
13659
|
+
isSignal: true,
|
|
13660
|
+
alias: "pinchZoomMax",
|
|
13661
|
+
required: false
|
|
13662
|
+
}]
|
|
13663
|
+
}]
|
|
13664
|
+
}
|
|
13665
|
+
});
|
|
13166
13666
|
const MARKDOWN_ZOOM_STEP = .1;
|
|
13167
13667
|
const MARKDOWN_ZOOM_MIN = .5;
|
|
13168
13668
|
const MARKDOWN_ZOOM_MAX = 2;
|
|
@@ -13585,7 +14085,11 @@ var PreviewContentComponent = class PreviewContentComponent {
|
|
|
13585
14085
|
div, not preview.js's iframe. Zoom is reimplemented locally below (the CSS zoom property, not
|
|
13586
14086
|
preview.js's transform+relayout dance — markdown content is far smaller than a converted
|
|
13587
14087
|
document, so that sophistication buys nothing here). -->
|
|
13588
|
-
<div
|
|
14088
|
+
<div
|
|
14089
|
+
[(pinchZoom)]="markdownZoom"
|
|
14090
|
+
[pinchZoomMin]="markdownZoomMin"
|
|
14091
|
+
[pinchZoomMax]="markdownZoomMax"
|
|
14092
|
+
class="h-full overflow-auto rounded-(--radius-sm) bg-(--bg-neutral-white) px-(--spacing-2xl) py-(--spacing-xl) shadow-(--shadow-1)">
|
|
13589
14093
|
<div class="prose max-w-none dark:prose-invert" [style.zoom]="markdownZoom()" [innerHTML]="markdownResource.value() | markdown"></div>
|
|
13590
14094
|
</div>
|
|
13591
14095
|
} @else {
|
|
@@ -13680,6 +14184,17 @@ var PreviewContentComponent = class PreviewContentComponent {
|
|
|
13680
14184
|
"zoomFit"
|
|
13681
14185
|
]
|
|
13682
14186
|
},
|
|
14187
|
+
{
|
|
14188
|
+
kind: "directive",
|
|
14189
|
+
type: PinchZoomDirective,
|
|
14190
|
+
selector: "[pinchZoom]",
|
|
14191
|
+
inputs: [
|
|
14192
|
+
"pinchZoom",
|
|
14193
|
+
"pinchZoomMin",
|
|
14194
|
+
"pinchZoomMax"
|
|
14195
|
+
],
|
|
14196
|
+
outputs: ["pinchZoomChange"]
|
|
14197
|
+
},
|
|
13683
14198
|
{
|
|
13684
14199
|
kind: "component",
|
|
13685
14200
|
type: AdvancedSearchTabs,
|
|
@@ -13779,6 +14294,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
13779
14294
|
imports: [
|
|
13780
14295
|
TranslocoPipe,
|
|
13781
14296
|
PreviewActionsComponent,
|
|
14297
|
+
PinchZoomDirective,
|
|
13782
14298
|
AdvancedSearchTabs,
|
|
13783
14299
|
LoadingDotsComponent,
|
|
13784
14300
|
ImageIcon,
|
|
@@ -13874,7 +14390,11 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
13874
14390
|
div, not preview.js's iframe. Zoom is reimplemented locally below (the CSS zoom property, not
|
|
13875
14391
|
preview.js's transform+relayout dance — markdown content is far smaller than a converted
|
|
13876
14392
|
document, so that sophistication buys nothing here). -->
|
|
13877
|
-
<div
|
|
14393
|
+
<div
|
|
14394
|
+
[(pinchZoom)]="markdownZoom"
|
|
14395
|
+
[pinchZoomMin]="markdownZoomMin"
|
|
14396
|
+
[pinchZoomMax]="markdownZoomMax"
|
|
14397
|
+
class="h-full overflow-auto rounded-(--radius-sm) bg-(--bg-neutral-white) px-(--spacing-2xl) py-(--spacing-xl) shadow-(--shadow-1)">
|
|
13878
14398
|
<div class="prose max-w-none dark:prose-invert" [style.zoom]="markdownZoom()" [innerHTML]="markdownResource.value() | markdown"></div>
|
|
13879
14399
|
</div>
|
|
13880
14400
|
} @else {
|
|
@@ -14109,7 +14629,7 @@ var PreviewHeaderComponent = class PreviewHeaderComponent {
|
|
|
14109
14629
|
@if (href()) {
|
|
14110
14630
|
<arrow-up-right-from-square-icon
|
|
14111
14631
|
aria-hidden="true"
|
|
14112
|
-
class="size-(--icon-sm) shrink-0 opacity-0 transition-opacity duration-150 group-hover/title:opacity-100 group-focus-visible/title:opacity-100" />
|
|
14632
|
+
class="size-(--icon-sm) shrink-0 opacity-0 transition-opacity duration-150 group-hover/title:opacity-100 group-focus-visible/title:opacity-100 pointer-coarse:opacity-100" />
|
|
14113
14633
|
}
|
|
14114
14634
|
</a>
|
|
14115
14635
|
|
|
@@ -14197,7 +14717,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
14197
14717
|
@if (href()) {
|
|
14198
14718
|
<arrow-up-right-from-square-icon
|
|
14199
14719
|
aria-hidden="true"
|
|
14200
|
-
class="size-(--icon-sm) shrink-0 opacity-0 transition-opacity duration-150 group-hover/title:opacity-100 group-focus-visible/title:opacity-100" />
|
|
14720
|
+
class="size-(--icon-sm) shrink-0 opacity-0 transition-opacity duration-150 group-hover/title:opacity-100 group-focus-visible/title:opacity-100 pointer-coarse:opacity-100" />
|
|
14201
14721
|
}
|
|
14202
14722
|
</a>
|
|
14203
14723
|
|
|
@@ -14536,7 +15056,7 @@ var SponsoredResultsComponent = class SponsoredResultsComponent {
|
|
|
14536
15056
|
@if (childElement()?.template) {
|
|
14537
15057
|
<ng-container [ngTemplateOutlet]="childElement()?.template" />
|
|
14538
15058
|
} @else {
|
|
14539
|
-
<span class="promoted-badge bg-(--bg-primary-base) invisible flex items-center rounded-xl px-2 text-xs text-white peer-hover:visible">PROMOTED</span>
|
|
15059
|
+
<span class="promoted-badge bg-(--bg-primary-base) invisible flex items-center rounded-xl px-2 text-xs text-white peer-hover:visible pointer-coarse:visible">PROMOTED</span>
|
|
14540
15060
|
}
|
|
14541
15061
|
}
|
|
14542
15062
|
</li>
|
|
@@ -14611,7 +15131,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
14611
15131
|
@if (childElement()?.template) {
|
|
14612
15132
|
<ng-container [ngTemplateOutlet]="childElement()?.template" />
|
|
14613
15133
|
} @else {
|
|
14614
|
-
<span class="promoted-badge bg-(--bg-primary-base) invisible flex items-center rounded-xl px-2 text-xs text-white peer-hover:visible">PROMOTED</span>
|
|
15134
|
+
<span class="promoted-badge bg-(--bg-primary-base) invisible flex items-center rounded-xl px-2 text-xs text-white peer-hover:visible pointer-coarse:visible">PROMOTED</span>
|
|
14615
15135
|
}
|
|
14616
15136
|
}
|
|
14617
15137
|
</li>
|
|
@@ -16379,6 +16899,8 @@ var AdvancedFiltersBodyComponent = class AdvancedFiltersBodyComponent {
|
|
|
16379
16899
|
"fieldLabelLang",
|
|
16380
16900
|
"placeholder",
|
|
16381
16901
|
"ariaLabel",
|
|
16902
|
+
"hideKeyboardLabel",
|
|
16903
|
+
"keyboardLabel",
|
|
16382
16904
|
"noResultsText",
|
|
16383
16905
|
"searchingText",
|
|
16384
16906
|
"errorText",
|
|
@@ -16893,6 +17415,7 @@ var ArticleEntities = class ArticleEntities {
|
|
|
16893
17415
|
cn = cn;
|
|
16894
17416
|
article = input.required(...ngDevMode ? [{ debugName: "article" }] : /* istanbul ignore next */ []);
|
|
16895
17417
|
state = injectEntitiesState(this.article);
|
|
17418
|
+
row = injectRowActivation(() => this.state.hovering);
|
|
16896
17419
|
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
16897
17420
|
minVersion: "12.0.0",
|
|
16898
17421
|
version: "22.2.1",
|
|
@@ -16928,30 +17451,24 @@ var ArticleEntities = class ArticleEntities {
|
|
|
16928
17451
|
<chevron-right class="transition-transform duration-150 group-open/parent:rotate-90" />
|
|
16929
17452
|
</summary>
|
|
16930
17453
|
|
|
16931
|
-
<List class="gap-
|
|
17454
|
+
<List class="gap-2 p-3 pt-0" selectionMode="explicit">
|
|
16932
17455
|
@for (data of highlight.metadata; track data.value) {
|
|
16933
17456
|
<ListItem size="medium"
|
|
16934
17457
|
[value]="data.value"
|
|
16935
|
-
class="select-none"
|
|
16936
|
-
(mouseenter)="
|
|
16937
|
-
(mouseleave)="
|
|
17458
|
+
class="select-none min-h-10 [&_.list-item-text]:text-(length:--size-lg)"
|
|
17459
|
+
(mouseenter)="row.enter(data.value)"
|
|
17460
|
+
(mouseleave)="row.leave()"
|
|
17461
|
+
(click)="row.activate(data.value)">
|
|
16938
17462
|
<ListItemText>{{ data.display }}</ListItemText>
|
|
16939
17463
|
|
|
16940
|
-
<
|
|
16941
|
-
|
|
16942
|
-
|
|
16943
|
-
|
|
16944
|
-
|
|
16945
|
-
|
|
16946
|
-
|
|
16947
|
-
|
|
16948
|
-
<button class="hover:scale-125" aria-label="next entity" (click)="state.navigateNext(highlight.entity, data)">
|
|
16949
|
-
<chevron-right />
|
|
16950
|
-
</button>
|
|
16951
|
-
} @else {
|
|
16952
|
-
{{ state.total(highlight.entity, data) }}
|
|
16953
|
-
}
|
|
16954
|
-
</span>
|
|
17464
|
+
<occurrence-navigator
|
|
17465
|
+
[active]="state.hovering() === data.value"
|
|
17466
|
+
[index]="state.hoverIndex()"
|
|
17467
|
+
[total]="state.total(highlight.entity, data)"
|
|
17468
|
+
previousLabel="previous entity"
|
|
17469
|
+
nextLabel="next entity"
|
|
17470
|
+
(previous)="state.navigatePrev(highlight.entity, data)"
|
|
17471
|
+
(next)="state.navigateNext(highlight.entity, data)" />
|
|
16955
17472
|
</ListItem>
|
|
16956
17473
|
}
|
|
16957
17474
|
</List>
|
|
@@ -17000,9 +17517,16 @@ var ArticleEntities = class ArticleEntities {
|
|
|
17000
17517
|
},
|
|
17001
17518
|
{
|
|
17002
17519
|
kind: "component",
|
|
17003
|
-
type:
|
|
17004
|
-
selector: "
|
|
17005
|
-
inputs: [
|
|
17520
|
+
type: OccurrenceNavigatorComponent,
|
|
17521
|
+
selector: "occurrence-navigator, OccurrenceNavigator",
|
|
17522
|
+
inputs: [
|
|
17523
|
+
"active",
|
|
17524
|
+
"index",
|
|
17525
|
+
"total",
|
|
17526
|
+
"previousLabel",
|
|
17527
|
+
"nextLabel"
|
|
17528
|
+
],
|
|
17529
|
+
outputs: ["previous", "next"]
|
|
17006
17530
|
},
|
|
17007
17531
|
{
|
|
17008
17532
|
kind: "pipe",
|
|
@@ -17027,7 +17551,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
17027
17551
|
ListItemComponent,
|
|
17028
17552
|
ListItemTextComponent,
|
|
17029
17553
|
ChevronRightIcon,
|
|
17030
|
-
|
|
17554
|
+
OccurrenceNavigatorComponent
|
|
17031
17555
|
],
|
|
17032
17556
|
template: `
|
|
17033
17557
|
@for (highlight of state.previewHighlights(); track highlight.entity) {
|
|
@@ -17042,30 +17566,24 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
17042
17566
|
<chevron-right class="transition-transform duration-150 group-open/parent:rotate-90" />
|
|
17043
17567
|
</summary>
|
|
17044
17568
|
|
|
17045
|
-
<List class="gap-
|
|
17569
|
+
<List class="gap-2 p-3 pt-0" selectionMode="explicit">
|
|
17046
17570
|
@for (data of highlight.metadata; track data.value) {
|
|
17047
17571
|
<ListItem size="medium"
|
|
17048
17572
|
[value]="data.value"
|
|
17049
|
-
class="select-none"
|
|
17050
|
-
(mouseenter)="
|
|
17051
|
-
(mouseleave)="
|
|
17573
|
+
class="select-none min-h-10 [&_.list-item-text]:text-(length:--size-lg)"
|
|
17574
|
+
(mouseenter)="row.enter(data.value)"
|
|
17575
|
+
(mouseleave)="row.leave()"
|
|
17576
|
+
(click)="row.activate(data.value)">
|
|
17052
17577
|
<ListItemText>{{ data.display }}</ListItemText>
|
|
17053
17578
|
|
|
17054
|
-
<
|
|
17055
|
-
|
|
17056
|
-
|
|
17057
|
-
|
|
17058
|
-
|
|
17059
|
-
|
|
17060
|
-
|
|
17061
|
-
|
|
17062
|
-
<button class="hover:scale-125" aria-label="next entity" (click)="state.navigateNext(highlight.entity, data)">
|
|
17063
|
-
<chevron-right />
|
|
17064
|
-
</button>
|
|
17065
|
-
} @else {
|
|
17066
|
-
{{ state.total(highlight.entity, data) }}
|
|
17067
|
-
}
|
|
17068
|
-
</span>
|
|
17579
|
+
<occurrence-navigator
|
|
17580
|
+
[active]="state.hovering() === data.value"
|
|
17581
|
+
[index]="state.hoverIndex()"
|
|
17582
|
+
[total]="state.total(highlight.entity, data)"
|
|
17583
|
+
previousLabel="previous entity"
|
|
17584
|
+
nextLabel="next entity"
|
|
17585
|
+
(previous)="state.navigatePrev(highlight.entity, data)"
|
|
17586
|
+
(next)="state.navigateNext(highlight.entity, data)" />
|
|
17069
17587
|
</ListItem>
|
|
17070
17588
|
}
|
|
17071
17589
|
</List>
|
|
@@ -17126,8 +17644,18 @@ var ArticleExtracts = class ArticleExtracts {
|
|
|
17126
17644
|
<div class="flex h-32 items-center justify-center">
|
|
17127
17645
|
<loading-dots size="md" class="text-(--font-primary-base)" [label]="'drawers.loadingExtracts' | transloco" />
|
|
17128
17646
|
</div>
|
|
17647
|
+
} @else if (state.extracts().length === 0) {
|
|
17648
|
+
<!-- The same room as the list below and the other panels (entities, snippet): under the header, 12px around. -->
|
|
17649
|
+
<div class="px-3 pb-3">
|
|
17650
|
+
<list-panel-empty color="green">
|
|
17651
|
+
<file-text-icon listPanelEmptyIcon />
|
|
17652
|
+
{{ 'drawers.noRelevantExtracts' | transloco }}
|
|
17653
|
+
<p listPanelEmptyHint>{{ 'drawers.noRelevantExtractsHint' | transloco }}</p>
|
|
17654
|
+
</list-panel-empty>
|
|
17655
|
+
</div>
|
|
17129
17656
|
} @else {
|
|
17130
|
-
|
|
17657
|
+
<!-- p-3 pt-0, as the lists of entities and snippet: the header above has its own 12px, the list needs them left, right and below. -->
|
|
17658
|
+
<List class="gap-1 p-3 pt-0" selectionMode="explicit">
|
|
17131
17659
|
@for (extract of state.extracts(); track extract.id) {
|
|
17132
17660
|
<ListItem
|
|
17133
17661
|
[value]="extract.textIndex"
|
|
@@ -17137,8 +17665,6 @@ var ArticleExtracts = class ArticleExtracts {
|
|
|
17137
17665
|
(keydown.space)="state.scrollTo(undefined, extract.textIndex, true)">
|
|
17138
17666
|
<p class="line-clamp-5" [innerHTML]="extract.text"></p>
|
|
17139
17667
|
</ListItem>
|
|
17140
|
-
} @empty {
|
|
17141
|
-
<p class="px-2 py-1 text-slate-300">{{ 'drawers.noRelevantExtracts' | transloco }}</p>
|
|
17142
17668
|
}
|
|
17143
17669
|
</List>
|
|
17144
17670
|
}
|
|
@@ -17153,6 +17679,12 @@ var ArticleExtracts = class ArticleExtracts {
|
|
|
17153
17679
|
selector: "chevron-right, chevron-right-icon, ChevronRight, ChevronRightIcon",
|
|
17154
17680
|
inputs: ["class"]
|
|
17155
17681
|
},
|
|
17682
|
+
{
|
|
17683
|
+
kind: "component",
|
|
17684
|
+
type: FileTextIcon,
|
|
17685
|
+
selector: "file-text, file-text-icon, FileText, FileTextIcon",
|
|
17686
|
+
inputs: ["class"]
|
|
17687
|
+
},
|
|
17156
17688
|
{
|
|
17157
17689
|
kind: "directive",
|
|
17158
17690
|
type: ListComponent,
|
|
@@ -17176,6 +17708,12 @@ var ArticleExtracts = class ArticleExtracts {
|
|
|
17176
17708
|
],
|
|
17177
17709
|
exportAs: ["listItem"]
|
|
17178
17710
|
},
|
|
17711
|
+
{
|
|
17712
|
+
kind: "component",
|
|
17713
|
+
type: ListPanelEmptyComponent,
|
|
17714
|
+
selector: "list-panel-empty, ListPanelEmpty",
|
|
17715
|
+
inputs: ["color"]
|
|
17716
|
+
},
|
|
17179
17717
|
{
|
|
17180
17718
|
kind: "component",
|
|
17181
17719
|
type: TagsIcon,
|
|
@@ -17208,8 +17746,10 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
17208
17746
|
imports: [
|
|
17209
17747
|
TranslocoPipe,
|
|
17210
17748
|
ChevronRightIcon,
|
|
17749
|
+
FileTextIcon,
|
|
17211
17750
|
ListComponent,
|
|
17212
17751
|
ListItemComponent,
|
|
17752
|
+
ListPanelEmptyComponent,
|
|
17213
17753
|
TagsIcon,
|
|
17214
17754
|
LoadingDotsComponent
|
|
17215
17755
|
],
|
|
@@ -17228,8 +17768,18 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
17228
17768
|
<div class="flex h-32 items-center justify-center">
|
|
17229
17769
|
<loading-dots size="md" class="text-(--font-primary-base)" [label]="'drawers.loadingExtracts' | transloco" />
|
|
17230
17770
|
</div>
|
|
17771
|
+
} @else if (state.extracts().length === 0) {
|
|
17772
|
+
<!-- The same room as the list below and the other panels (entities, snippet): under the header, 12px around. -->
|
|
17773
|
+
<div class="px-3 pb-3">
|
|
17774
|
+
<list-panel-empty color="green">
|
|
17775
|
+
<file-text-icon listPanelEmptyIcon />
|
|
17776
|
+
{{ 'drawers.noRelevantExtracts' | transloco }}
|
|
17777
|
+
<p listPanelEmptyHint>{{ 'drawers.noRelevantExtractsHint' | transloco }}</p>
|
|
17778
|
+
</list-panel-empty>
|
|
17779
|
+
</div>
|
|
17231
17780
|
} @else {
|
|
17232
|
-
|
|
17781
|
+
<!-- p-3 pt-0, as the lists of entities and snippet: the header above has its own 12px, the list needs them left, right and below. -->
|
|
17782
|
+
<List class="gap-1 p-3 pt-0" selectionMode="explicit">
|
|
17233
17783
|
@for (extract of state.extracts(); track extract.id) {
|
|
17234
17784
|
<ListItem
|
|
17235
17785
|
[value]="extract.textIndex"
|
|
@@ -17239,8 +17789,6 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
17239
17789
|
(keydown.space)="state.scrollTo(undefined, extract.textIndex, true)">
|
|
17240
17790
|
<p class="line-clamp-5" [innerHTML]="extract.text"></p>
|
|
17241
17791
|
</ListItem>
|
|
17242
|
-
} @empty {
|
|
17243
|
-
<p class="px-2 py-1 text-slate-300">{{ 'drawers.noRelevantExtracts' | transloco }}</p>
|
|
17244
17792
|
}
|
|
17245
17793
|
</List>
|
|
17246
17794
|
}
|
|
@@ -17428,6 +17976,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
17428
17976
|
});
|
|
17429
17977
|
var ArticleMatchLocations = class ArticleMatchLocations {
|
|
17430
17978
|
state = injectMatchLocationsState();
|
|
17979
|
+
row = injectRowActivation(() => this.state.hovering);
|
|
17431
17980
|
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
17432
17981
|
minVersion: "12.0.0",
|
|
17433
17982
|
version: "22.2.1",
|
|
@@ -17455,31 +18004,25 @@ var ArticleMatchLocations = class ArticleMatchLocations {
|
|
|
17455
18004
|
<chevron-right class="transition-transform duration-150 group-open/parent:rotate-90" />
|
|
17456
18005
|
</summary>
|
|
17457
18006
|
|
|
17458
|
-
<List class="gap-
|
|
18007
|
+
<List class="gap-2 p-3 pt-0" selectionMode="explicit">
|
|
17459
18008
|
@for (data of state.values(); track data.value) {
|
|
17460
18009
|
<ListItem
|
|
17461
18010
|
size="medium"
|
|
17462
18011
|
[value]="data.value"
|
|
17463
|
-
class="select-none"
|
|
17464
|
-
(mouseenter)="
|
|
17465
|
-
(mouseleave)="
|
|
18012
|
+
class="select-none min-h-10 [&_.list-item-text]:text-(length:--size-lg)"
|
|
18013
|
+
(mouseenter)="row.enter(data.value)"
|
|
18014
|
+
(mouseleave)="row.leave()"
|
|
18015
|
+
(click)="row.activate(data.value)">
|
|
17466
18016
|
<ListItemText>{{ data.display }}</ListItemText>
|
|
17467
18017
|
|
|
17468
|
-
<
|
|
17469
|
-
|
|
17470
|
-
|
|
17471
|
-
|
|
17472
|
-
|
|
17473
|
-
|
|
17474
|
-
|
|
17475
|
-
|
|
17476
|
-
<button class="hover:scale-125" aria-label="next match" (click)="state.navigateNext(data)">
|
|
17477
|
-
<chevron-right />
|
|
17478
|
-
</button>
|
|
17479
|
-
} @else {
|
|
17480
|
-
{{ data.count }}
|
|
17481
|
-
}
|
|
17482
|
-
</span>
|
|
18018
|
+
<occurrence-navigator
|
|
18019
|
+
[active]="state.hovering() === data.value"
|
|
18020
|
+
[index]="state.hoverIndex()"
|
|
18021
|
+
[total]="data.count ?? 0"
|
|
18022
|
+
previousLabel="previous match"
|
|
18023
|
+
nextLabel="next match"
|
|
18024
|
+
(previous)="state.navigatePrev(data)"
|
|
18025
|
+
(next)="state.navigateNext(data)" />
|
|
17483
18026
|
</ListItem>
|
|
17484
18027
|
}
|
|
17485
18028
|
</List>
|
|
@@ -17521,15 +18064,22 @@ var ArticleMatchLocations = class ArticleMatchLocations {
|
|
|
17521
18064
|
},
|
|
17522
18065
|
{
|
|
17523
18066
|
kind: "component",
|
|
17524
|
-
type:
|
|
17525
|
-
selector: "chevron-
|
|
18067
|
+
type: ChevronRightIcon,
|
|
18068
|
+
selector: "chevron-right, chevron-right-icon, ChevronRight, ChevronRightIcon",
|
|
17526
18069
|
inputs: ["class"]
|
|
17527
18070
|
},
|
|
17528
18071
|
{
|
|
17529
18072
|
kind: "component",
|
|
17530
|
-
type:
|
|
17531
|
-
selector: "
|
|
17532
|
-
inputs: [
|
|
18073
|
+
type: OccurrenceNavigatorComponent,
|
|
18074
|
+
selector: "occurrence-navigator, OccurrenceNavigator",
|
|
18075
|
+
inputs: [
|
|
18076
|
+
"active",
|
|
18077
|
+
"index",
|
|
18078
|
+
"total",
|
|
18079
|
+
"previousLabel",
|
|
18080
|
+
"nextLabel"
|
|
18081
|
+
],
|
|
18082
|
+
outputs: ["previous", "next"]
|
|
17533
18083
|
},
|
|
17534
18084
|
{
|
|
17535
18085
|
kind: "pipe",
|
|
@@ -17553,8 +18103,8 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
17553
18103
|
ListComponent,
|
|
17554
18104
|
ListItemComponent,
|
|
17555
18105
|
ListItemTextComponent,
|
|
17556
|
-
|
|
17557
|
-
|
|
18106
|
+
ChevronRightIcon,
|
|
18107
|
+
OccurrenceNavigatorComponent
|
|
17558
18108
|
],
|
|
17559
18109
|
template: `
|
|
17560
18110
|
@if (state.values().length) {
|
|
@@ -17568,31 +18118,25 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
17568
18118
|
<chevron-right class="transition-transform duration-150 group-open/parent:rotate-90" />
|
|
17569
18119
|
</summary>
|
|
17570
18120
|
|
|
17571
|
-
<List class="gap-
|
|
18121
|
+
<List class="gap-2 p-3 pt-0" selectionMode="explicit">
|
|
17572
18122
|
@for (data of state.values(); track data.value) {
|
|
17573
18123
|
<ListItem
|
|
17574
18124
|
size="medium"
|
|
17575
18125
|
[value]="data.value"
|
|
17576
|
-
class="select-none"
|
|
17577
|
-
(mouseenter)="
|
|
17578
|
-
(mouseleave)="
|
|
18126
|
+
class="select-none min-h-10 [&_.list-item-text]:text-(length:--size-lg)"
|
|
18127
|
+
(mouseenter)="row.enter(data.value)"
|
|
18128
|
+
(mouseleave)="row.leave()"
|
|
18129
|
+
(click)="row.activate(data.value)">
|
|
17579
18130
|
<ListItemText>{{ data.display }}</ListItemText>
|
|
17580
18131
|
|
|
17581
|
-
<
|
|
17582
|
-
|
|
17583
|
-
|
|
17584
|
-
|
|
17585
|
-
|
|
17586
|
-
|
|
17587
|
-
|
|
17588
|
-
|
|
17589
|
-
<button class="hover:scale-125" aria-label="next match" (click)="state.navigateNext(data)">
|
|
17590
|
-
<chevron-right />
|
|
17591
|
-
</button>
|
|
17592
|
-
} @else {
|
|
17593
|
-
{{ data.count }}
|
|
17594
|
-
}
|
|
17595
|
-
</span>
|
|
18132
|
+
<occurrence-navigator
|
|
18133
|
+
[active]="state.hovering() === data.value"
|
|
18134
|
+
[index]="state.hoverIndex()"
|
|
18135
|
+
[total]="data.count ?? 0"
|
|
18136
|
+
previousLabel="previous match"
|
|
18137
|
+
nextLabel="next match"
|
|
18138
|
+
(previous)="state.navigatePrev(data)"
|
|
18139
|
+
(next)="state.navigateNext(data)" />
|
|
17596
18140
|
</ListItem>
|
|
17597
18141
|
}
|
|
17598
18142
|
</List>
|
|
@@ -19217,6 +19761,15 @@ var AlertsComponent = class AlertsComponent {
|
|
|
19217
19761
|
return new Map(this.order().map((alert) => [alert, alertSummary(lang, alert)]));
|
|
19218
19762
|
}, ...ngDevMode ? [{ debugName: "summaries" }] : /* istanbul ignore next */ []);
|
|
19219
19763
|
summaryOf = (alert) => this.summaries().get(alert) ?? [];
|
|
19764
|
+
keyboardAvailable = injectKeyboardAvailable();
|
|
19765
|
+
fullText(alert) {
|
|
19766
|
+
const summary = this.summaryOf(alert).map((part) => part.key ? this.transloco.translate(part.key) : part.text).join(" · ");
|
|
19767
|
+
return summary ? `${alert.name} · ${summary}` : alert.name;
|
|
19768
|
+
}
|
|
19769
|
+
toggleText(tip) {
|
|
19770
|
+
if (tip.isOpen()) tip.hide();
|
|
19771
|
+
else tip.show();
|
|
19772
|
+
}
|
|
19220
19773
|
announceMove = (alert, position, total) => this.transloco.translate("alerts.moved", {
|
|
19221
19774
|
name: alert.name,
|
|
19222
19775
|
position,
|
|
@@ -19348,7 +19901,7 @@ var AlertsComponent = class AlertsComponent {
|
|
|
19348
19901
|
},
|
|
19349
19902
|
providers: [provideTranslocoScope("alerts")],
|
|
19350
19903
|
ngImport: i0,
|
|
19351
|
-
template: "<div
|
|
19904
|
+
template: "<!-- A minimum width and 8px of padding all round are for a floating popover, which has nothing around the list and would collapse on a\n short one. In a panel they were wrong: the minimum made the list wider than the room it is given (320px in the 286px of the settings\n on a phone, clipped) and the padding pushed its rows 8px in from the headings of the page, where the container sets both. -->\n<div\n class=\"flex flex-col gap-(--spacing-3xs)\"\n [class.min-w-80]=\"!!floating\"\n [class.p-2]=\"!!floating\">\n <div class=\"flex items-center gap-(--spacing-2xs)\" [class.ps-2]=\"!!floating\">\n @if (floating) {\n <!-- Not an <h2>: a page's prose styles put a rule and margins on every h2, even inside a popover. -->\n <span\n role=\"heading\"\n aria-level=\"2\"\n class=\"flex-1 text-xl leading-(--line-height-4) font-bold\">\n {{ \"alerts.label\" | transloco }}\n @if (visible().length) {\n <span\n class=\"text-(length:--size-sm) font-normal text-(--font-neutral-muted)\"\n >· {{ visible().length }}</span\n >\n }\n </span>\n } @else {\n <span class=\"flex-1\"></span>\n }\n <!-- With no alert, \"Create\" is the call to action of the empty state, below. -->\n @if (visible().length) {\n <button size=\"md\" (click)=\"createAlert()\">\n <plus-icon />\n {{ \"alerts.create\" | transloco }}\n </button>\n }\n </div>\n\n @if (saveError(); as failure) {\n <alert-save-error class=\"mb-(--spacing-2xs)\" [detail]=\"failure.detail\" />\n }\n\n <ul\n role=\"list\"\n class=\"m-0 flex list-none flex-col p-0\"\n [attr.aria-label]=\"'alerts.label' | transloco\"\n [sortable]=\"visible()\"\n #sortable=\"sortable\"\n [sortableAnnounce]=\"announceMove\"\n (reorder)=\"saveOrder($event)\">\n @for (alert of sortable.items(); track alert.name) {\n @let name = alert.name;\n <li\n [sortableItem]=\"alert\"\n class=\"group relative block w-full cursor-pointer rounded-(--radius-sm) text-(--font-neutral-base) select-none hover:bg-(--bg-primary-lighter) focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-(--stroke-focus) active:bg-(--bg-primary-light) data-dragging:bg-(--bg-primary-lighter) data-dragging:outline-2 data-dragging:-outline-offset-2 data-dragging:outline-(--stroke-primary-base) data-dragging:outline-dashed data-dragging:*:invisible\"\n tabindex=\"0\"\n [attr.data-inactive]=\"alert.active ? null : ''\"\n (click)=\"onRowClick($event, alert)\"\n (keydown.enter)=\"onRowEnter($event, alert)\">\n <!-- Touch: drag the row to the left to delete it (the undo notice below is the safety net). The cross at the end of the row is\n then out of sight (`pointer-coarse:sr-only`) but still there for a screen reader or a switch, which cannot swipe. The\n layout of the row is the sliding layer's: the row itself only carries the hover tint, the focus ring and the drag look. -->\n <SwipeAction\n class=\"rounded-(--radius-sm)\"\n contentClass=\"flex min-h-12 w-full items-center gap-(--spacing-3xs) py-(--spacing-3xs) ps-(--spacing-3xs) pe-(--spacing-2xs)\"\n endEnabled\n endTone=\"error\"\n (swipedEnd)=\"deleteAlert(alert)\">\n <span swipeActionEnd class=\"flex items-center gap-2\">\n {{ \"alerts.deleteAlert\" | transloco }}\n <XMarkIcon class=\"size-4\" />\n </span>\n <!-- Handle: shown when the row is hovered or focused, only if there is something to reorder. -->\n <grip-vertical-icon\n class=\"shrink-0 cursor-grab text-(--font-neutral-muted) opacity-0 group-focus-within:opacity-100 group-hover:opacity-100 pointer-coarse:hidden\"\n [class.invisible]=\"visible().length < 2\" />\n @if (alert.active) {\n <bell-icon class=\"shrink-0\" />\n } @else {\n <bell-slash-icon class=\"shrink-0 text-(--font-neutral-muted)\" />\n }\n <!-- The text block carries the tooltip that shows the name and the second line whole, when either is cut off (a mouse hovers it;\n a finger has the info button below). Both lines are always in the DOM, the second one hidden when there is nothing in it, so\n that the row can ask each whether it is cut off. -->\n <span\n #tip=\"tooltip\"\n class=\"flex min-w-0 flex-1 flex-col\"\n [tooltip]=\"fullText(alert)\"\n [tooltipDisabled]=\"\n !(cutName.isTruncated() || cutSummary.isTruncated())\n \">\n <span\n truncated\n #cutName=\"truncated\"\n class=\"line-clamp-1 text-(length:--size-base) group-data-inactive:text-(--font-neutral-muted)\"\n >{{ name }}</span\n >\n <span\n truncated\n #cutSummary=\"truncated\"\n [class]=\"\n summaryOf(alert).length\n ? 'line-clamp-1 text-(length:--size-sm) text-(--font-neutral-muted)'\n : 'hidden'\n \">\n @for (\n part of summaryOf(alert);\n track $index;\n let first = $first\n ) {\n {{ first ? \"\" : \" · \"\n }}{{ part.key ? (part.key | transloco) : part.text }}\n }\n </span>\n </span>\n @if (cutName.isTruncated() || cutSummary.isTruncated()) {\n <!-- Touch only: there is no hover to show the tooltip with. In data-row-action, so a tap on it does not open the form of the row. -->\n <span data-row-action class=\"flex shrink-0 items-center\">\n <button\n type=\"button\"\n variant=\"tertiary\"\n size=\"sm\"\n [iconOnly]=\"true\"\n class=\"hidden text-(--font-neutral-muted) pointer-coarse:inline-flex pointer-coarse:size-9\"\n [attr.aria-label]=\"'alerts.showFullText' | transloco\"\n (click)=\"toggleText(tip)\">\n <InfoCircleIcon />\n </button>\n </span>\n }\n <span data-row-action class=\"flex shrink-0 items-center\">\n <Switch\n [checked]=\"isActive(alert)\"\n [aria-label]=\"'alerts.alertActiveFor' | transloco: { name }\"\n (checkedChange)=\"toggle(alert, $event)\" />\n </span>\n <span data-row-action class=\"flex shrink-0 items-center\">\n <button\n variant=\"tertiary\"\n size=\"sm\"\n [iconOnly]=\"true\"\n class=\"text-(--font-error-muted) opacity-0 group-focus-within:opacity-100 group-hover:opacity-100 hover:bg-(--bg-error-lightest) hover:text-(--font-error-base) focus-visible:opacity-100 pointer-coarse:sr-only\"\n [attr.title]=\"'alerts.deleteAlertNamed' | transloco: { name }\"\n [attr.aria-label]=\"\n 'alerts.deleteAlertNamed' | transloco: { name }\n \"\n (click)=\"deleteAlert(alert)\">\n <XMarkIcon />\n </button>\n </span>\n </SwipeAction>\n </li>\n } @empty {\n <li class=\"list-none\">\n <ListPanelEmpty color=\"orange\">\n <bell-icon listPanelEmptyIcon />\n {{ \"alerts.noAlerts\" | transloco }}\n <p listPanelEmptyHint>{{ \"alerts.noAlertsHint\" | transloco }}</p>\n <button listPanelEmptyAction (click)=\"createAlert()\">\n <plus-icon />\n {{ \"alerts.createAlert\" | transloco }}\n </button>\n </ListPanelEmpty>\n </li>\n }\n </ul>\n\n <!-- The keys to reorder a row mean nothing to a finger (the handle is hidden there too), and nothing without a keyboard. -->\n @if (visible().length > 1 && keyboardAvailable()) {\n <p\n class=\"m-0 pt-(--spacing-3xs) text-(length:--size-sm) text-(--font-neutral-muted)\"\n [class.ps-2]=\"!!floating\">\n {{ \"alerts.reorderHint\" | transloco }}\n </p>\n }\n\n <!-- The regions exist before their text does, which is what makes a screen reader announce the text. -->\n <div role=\"status\">\n @if (pendingDeletion(); as pending) {\n <div\n class=\"mt-(--spacing-2xs) flex items-center gap-(--spacing-2xs) rounded-(--radius-sm) bg-(--bg-neutral-base) py-(--spacing-3xs) ps-(--spacing-xs) pe-(--spacing-3xs) text-(length:--size-sm) text-(--font-neutral-base)\">\n <span class=\"min-w-0 flex-1\">{{\n \"alerts.deleted\" | transloco: { name: pending.name }\n }}</span>\n <button variant=\"tertiary\" size=\"sm\" (click)=\"undoDeletion()\">\n {{ \"alerts.undo\" | transloco }}\n </button>\n </div>\n }\n </div>\n</div>\n",
|
|
19352
19905
|
dependencies: [
|
|
19353
19906
|
{
|
|
19354
19907
|
kind: "directive",
|
|
@@ -19397,6 +19950,22 @@ var AlertsComponent = class AlertsComponent {
|
|
|
19397
19950
|
selector: "[sortableItem]",
|
|
19398
19951
|
inputs: ["sortableItem", "sortableItemDisabled"]
|
|
19399
19952
|
},
|
|
19953
|
+
{
|
|
19954
|
+
kind: "component",
|
|
19955
|
+
type: SwipeActionComponent,
|
|
19956
|
+
selector: "swipe-action, SwipeAction",
|
|
19957
|
+
inputs: [
|
|
19958
|
+
"class",
|
|
19959
|
+
"contentClass",
|
|
19960
|
+
"startEnabled",
|
|
19961
|
+
"endEnabled",
|
|
19962
|
+
"startTone",
|
|
19963
|
+
"endTone",
|
|
19964
|
+
"disabled",
|
|
19965
|
+
"threshold"
|
|
19966
|
+
],
|
|
19967
|
+
outputs: ["swipedStart", "swipedEnd"]
|
|
19968
|
+
},
|
|
19400
19969
|
{
|
|
19401
19970
|
kind: "component",
|
|
19402
19971
|
type: BellIcon,
|
|
@@ -19415,16 +19984,55 @@ var AlertsComponent = class AlertsComponent {
|
|
|
19415
19984
|
selector: "grip-vertical, grip-vertical-icon, GripVertical, GripVerticalIcon",
|
|
19416
19985
|
inputs: ["class"]
|
|
19417
19986
|
},
|
|
19987
|
+
{
|
|
19988
|
+
kind: "component",
|
|
19989
|
+
type: InfoCircleIcon,
|
|
19990
|
+
selector: "info-circle, info-circle-icon, InfoCircle, InfoCircleIcon",
|
|
19991
|
+
inputs: ["class"]
|
|
19992
|
+
},
|
|
19418
19993
|
{
|
|
19419
19994
|
kind: "component",
|
|
19420
19995
|
type: PlusIcon,
|
|
19421
19996
|
selector: "plus, plus-icon, Plus, PlusIcon",
|
|
19422
19997
|
inputs: ["class"]
|
|
19423
19998
|
},
|
|
19999
|
+
{
|
|
20000
|
+
kind: "directive",
|
|
20001
|
+
type: TooltipDirective,
|
|
20002
|
+
selector: "[tooltip]",
|
|
20003
|
+
inputs: [
|
|
20004
|
+
"tooltip",
|
|
20005
|
+
"tooltipPosition",
|
|
20006
|
+
"tooltipEvent",
|
|
20007
|
+
"showDelay",
|
|
20008
|
+
"hideDelay",
|
|
20009
|
+
"life",
|
|
20010
|
+
"tooltipOffset",
|
|
20011
|
+
"autoHide",
|
|
20012
|
+
"hideOnEscape",
|
|
20013
|
+
"escape",
|
|
20014
|
+
"tooltipDisabled",
|
|
20015
|
+
"tooltipArrow",
|
|
20016
|
+
"tooltipStyleClass",
|
|
20017
|
+
"tooltipOptions",
|
|
20018
|
+
"tooltip-position",
|
|
20019
|
+
"tooltip-delay",
|
|
20020
|
+
"tooltip-duration",
|
|
20021
|
+
"tooltip-offset"
|
|
20022
|
+
],
|
|
20023
|
+
exportAs: ["tooltip"]
|
|
20024
|
+
},
|
|
20025
|
+
{
|
|
20026
|
+
kind: "directive",
|
|
20027
|
+
type: TruncatedDirective,
|
|
20028
|
+
selector: "[truncated]",
|
|
20029
|
+
inputs: ["truncated"],
|
|
20030
|
+
exportAs: ["truncated"]
|
|
20031
|
+
},
|
|
19424
20032
|
{
|
|
19425
20033
|
kind: "component",
|
|
19426
|
-
type:
|
|
19427
|
-
selector: "
|
|
20034
|
+
type: XMarkIcon,
|
|
20035
|
+
selector: "x-mark, x-mark-icon, XMark, XMarkIcon",
|
|
19428
20036
|
inputs: ["class"]
|
|
19429
20037
|
},
|
|
19430
20038
|
{
|
|
@@ -19462,16 +20070,20 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
19462
20070
|
SwitchComponent,
|
|
19463
20071
|
SortableDirective,
|
|
19464
20072
|
SortableItemDirective,
|
|
20073
|
+
SwipeActionComponent,
|
|
19465
20074
|
BellIcon,
|
|
19466
20075
|
BellSlashIcon,
|
|
19467
20076
|
GripVerticalIcon,
|
|
20077
|
+
InfoCircleIcon,
|
|
19468
20078
|
PlusIcon,
|
|
19469
|
-
|
|
20079
|
+
TooltipDirective,
|
|
20080
|
+
TruncatedDirective,
|
|
20081
|
+
XMarkIcon,
|
|
19470
20082
|
ListPanelEmptyComponent,
|
|
19471
20083
|
AlertSaveErrorComponent
|
|
19472
20084
|
],
|
|
19473
20085
|
providers: [provideTranslocoScope("alerts")],
|
|
19474
|
-
template: "<div
|
|
20086
|
+
template: "<!-- A minimum width and 8px of padding all round are for a floating popover, which has nothing around the list and would collapse on a\n short one. In a panel they were wrong: the minimum made the list wider than the room it is given (320px in the 286px of the settings\n on a phone, clipped) and the padding pushed its rows 8px in from the headings of the page, where the container sets both. -->\n<div\n class=\"flex flex-col gap-(--spacing-3xs)\"\n [class.min-w-80]=\"!!floating\"\n [class.p-2]=\"!!floating\">\n <div class=\"flex items-center gap-(--spacing-2xs)\" [class.ps-2]=\"!!floating\">\n @if (floating) {\n <!-- Not an <h2>: a page's prose styles put a rule and margins on every h2, even inside a popover. -->\n <span\n role=\"heading\"\n aria-level=\"2\"\n class=\"flex-1 text-xl leading-(--line-height-4) font-bold\">\n {{ \"alerts.label\" | transloco }}\n @if (visible().length) {\n <span\n class=\"text-(length:--size-sm) font-normal text-(--font-neutral-muted)\"\n >· {{ visible().length }}</span\n >\n }\n </span>\n } @else {\n <span class=\"flex-1\"></span>\n }\n <!-- With no alert, \"Create\" is the call to action of the empty state, below. -->\n @if (visible().length) {\n <button size=\"md\" (click)=\"createAlert()\">\n <plus-icon />\n {{ \"alerts.create\" | transloco }}\n </button>\n }\n </div>\n\n @if (saveError(); as failure) {\n <alert-save-error class=\"mb-(--spacing-2xs)\" [detail]=\"failure.detail\" />\n }\n\n <ul\n role=\"list\"\n class=\"m-0 flex list-none flex-col p-0\"\n [attr.aria-label]=\"'alerts.label' | transloco\"\n [sortable]=\"visible()\"\n #sortable=\"sortable\"\n [sortableAnnounce]=\"announceMove\"\n (reorder)=\"saveOrder($event)\">\n @for (alert of sortable.items(); track alert.name) {\n @let name = alert.name;\n <li\n [sortableItem]=\"alert\"\n class=\"group relative block w-full cursor-pointer rounded-(--radius-sm) text-(--font-neutral-base) select-none hover:bg-(--bg-primary-lighter) focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-(--stroke-focus) active:bg-(--bg-primary-light) data-dragging:bg-(--bg-primary-lighter) data-dragging:outline-2 data-dragging:-outline-offset-2 data-dragging:outline-(--stroke-primary-base) data-dragging:outline-dashed data-dragging:*:invisible\"\n tabindex=\"0\"\n [attr.data-inactive]=\"alert.active ? null : ''\"\n (click)=\"onRowClick($event, alert)\"\n (keydown.enter)=\"onRowEnter($event, alert)\">\n <!-- Touch: drag the row to the left to delete it (the undo notice below is the safety net). The cross at the end of the row is\n then out of sight (`pointer-coarse:sr-only`) but still there for a screen reader or a switch, which cannot swipe. The\n layout of the row is the sliding layer's: the row itself only carries the hover tint, the focus ring and the drag look. -->\n <SwipeAction\n class=\"rounded-(--radius-sm)\"\n contentClass=\"flex min-h-12 w-full items-center gap-(--spacing-3xs) py-(--spacing-3xs) ps-(--spacing-3xs) pe-(--spacing-2xs)\"\n endEnabled\n endTone=\"error\"\n (swipedEnd)=\"deleteAlert(alert)\">\n <span swipeActionEnd class=\"flex items-center gap-2\">\n {{ \"alerts.deleteAlert\" | transloco }}\n <XMarkIcon class=\"size-4\" />\n </span>\n <!-- Handle: shown when the row is hovered or focused, only if there is something to reorder. -->\n <grip-vertical-icon\n class=\"shrink-0 cursor-grab text-(--font-neutral-muted) opacity-0 group-focus-within:opacity-100 group-hover:opacity-100 pointer-coarse:hidden\"\n [class.invisible]=\"visible().length < 2\" />\n @if (alert.active) {\n <bell-icon class=\"shrink-0\" />\n } @else {\n <bell-slash-icon class=\"shrink-0 text-(--font-neutral-muted)\" />\n }\n <!-- The text block carries the tooltip that shows the name and the second line whole, when either is cut off (a mouse hovers it;\n a finger has the info button below). Both lines are always in the DOM, the second one hidden when there is nothing in it, so\n that the row can ask each whether it is cut off. -->\n <span\n #tip=\"tooltip\"\n class=\"flex min-w-0 flex-1 flex-col\"\n [tooltip]=\"fullText(alert)\"\n [tooltipDisabled]=\"\n !(cutName.isTruncated() || cutSummary.isTruncated())\n \">\n <span\n truncated\n #cutName=\"truncated\"\n class=\"line-clamp-1 text-(length:--size-base) group-data-inactive:text-(--font-neutral-muted)\"\n >{{ name }}</span\n >\n <span\n truncated\n #cutSummary=\"truncated\"\n [class]=\"\n summaryOf(alert).length\n ? 'line-clamp-1 text-(length:--size-sm) text-(--font-neutral-muted)'\n : 'hidden'\n \">\n @for (\n part of summaryOf(alert);\n track $index;\n let first = $first\n ) {\n {{ first ? \"\" : \" · \"\n }}{{ part.key ? (part.key | transloco) : part.text }}\n }\n </span>\n </span>\n @if (cutName.isTruncated() || cutSummary.isTruncated()) {\n <!-- Touch only: there is no hover to show the tooltip with. In data-row-action, so a tap on it does not open the form of the row. -->\n <span data-row-action class=\"flex shrink-0 items-center\">\n <button\n type=\"button\"\n variant=\"tertiary\"\n size=\"sm\"\n [iconOnly]=\"true\"\n class=\"hidden text-(--font-neutral-muted) pointer-coarse:inline-flex pointer-coarse:size-9\"\n [attr.aria-label]=\"'alerts.showFullText' | transloco\"\n (click)=\"toggleText(tip)\">\n <InfoCircleIcon />\n </button>\n </span>\n }\n <span data-row-action class=\"flex shrink-0 items-center\">\n <Switch\n [checked]=\"isActive(alert)\"\n [aria-label]=\"'alerts.alertActiveFor' | transloco: { name }\"\n (checkedChange)=\"toggle(alert, $event)\" />\n </span>\n <span data-row-action class=\"flex shrink-0 items-center\">\n <button\n variant=\"tertiary\"\n size=\"sm\"\n [iconOnly]=\"true\"\n class=\"text-(--font-error-muted) opacity-0 group-focus-within:opacity-100 group-hover:opacity-100 hover:bg-(--bg-error-lightest) hover:text-(--font-error-base) focus-visible:opacity-100 pointer-coarse:sr-only\"\n [attr.title]=\"'alerts.deleteAlertNamed' | transloco: { name }\"\n [attr.aria-label]=\"\n 'alerts.deleteAlertNamed' | transloco: { name }\n \"\n (click)=\"deleteAlert(alert)\">\n <XMarkIcon />\n </button>\n </span>\n </SwipeAction>\n </li>\n } @empty {\n <li class=\"list-none\">\n <ListPanelEmpty color=\"orange\">\n <bell-icon listPanelEmptyIcon />\n {{ \"alerts.noAlerts\" | transloco }}\n <p listPanelEmptyHint>{{ \"alerts.noAlertsHint\" | transloco }}</p>\n <button listPanelEmptyAction (click)=\"createAlert()\">\n <plus-icon />\n {{ \"alerts.createAlert\" | transloco }}\n </button>\n </ListPanelEmpty>\n </li>\n }\n </ul>\n\n <!-- The keys to reorder a row mean nothing to a finger (the handle is hidden there too), and nothing without a keyboard. -->\n @if (visible().length > 1 && keyboardAvailable()) {\n <p\n class=\"m-0 pt-(--spacing-3xs) text-(length:--size-sm) text-(--font-neutral-muted)\"\n [class.ps-2]=\"!!floating\">\n {{ \"alerts.reorderHint\" | transloco }}\n </p>\n }\n\n <!-- The regions exist before their text does, which is what makes a screen reader announce the text. -->\n <div role=\"status\">\n @if (pendingDeletion(); as pending) {\n <div\n class=\"mt-(--spacing-2xs) flex items-center gap-(--spacing-2xs) rounded-(--radius-sm) bg-(--bg-neutral-base) py-(--spacing-3xs) ps-(--spacing-xs) pe-(--spacing-3xs) text-(length:--size-sm) text-(--font-neutral-base)\">\n <span class=\"min-w-0 flex-1\">{{\n \"alerts.deleted\" | transloco: { name: pending.name }\n }}</span>\n <button variant=\"tertiary\" size=\"sm\" (click)=\"undoDeletion()\">\n {{ \"alerts.undo\" | transloco }}\n </button>\n </div>\n }\n </div>\n</div>\n"
|
|
19475
20087
|
}]
|
|
19476
20088
|
}],
|
|
19477
20089
|
ctorParameters: () => [],
|
|
@@ -21384,7 +21996,7 @@ var ListPanelComponent = class ListPanelComponent {
|
|
|
21384
21996
|
<ListItemAction [ariaLabel]="deleteButtonTitle() | transloco" [keys]="deleteKeys" [shortcut]="'listPanel.deleteShortcut' | transloco"
|
|
21385
21997
|
[description]="'listPanel.deleteDescription' | transloco: { key: 'listPanel.deleteShortcut' | transloco }"
|
|
21386
21998
|
(click)="onDelete(item, i, $event)">
|
|
21387
|
-
<
|
|
21999
|
+
<XMarkIcon />
|
|
21388
22000
|
</ListItemAction>
|
|
21389
22001
|
}
|
|
21390
22002
|
</ListItem>
|
|
@@ -21410,8 +22022,8 @@ var ListPanelComponent = class ListPanelComponent {
|
|
|
21410
22022
|
},
|
|
21411
22023
|
{
|
|
21412
22024
|
kind: "component",
|
|
21413
|
-
type:
|
|
21414
|
-
selector: "
|
|
22025
|
+
type: XMarkIcon,
|
|
22026
|
+
selector: "x-mark, x-mark-icon, XMark, XMarkIcon",
|
|
21415
22027
|
inputs: ["class"]
|
|
21416
22028
|
},
|
|
21417
22029
|
{
|
|
@@ -21470,7 +22082,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
21470
22082
|
providers: [provideTranslocoScope("list-panel")],
|
|
21471
22083
|
imports: [
|
|
21472
22084
|
NgTemplateOutlet,
|
|
21473
|
-
|
|
22085
|
+
XMarkIcon,
|
|
21474
22086
|
TranslocoPipe,
|
|
21475
22087
|
ListComponent,
|
|
21476
22088
|
ListItemComponent,
|
|
@@ -21491,7 +22103,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
21491
22103
|
<ListItemAction [ariaLabel]="deleteButtonTitle() | transloco" [keys]="deleteKeys" [shortcut]="'listPanel.deleteShortcut' | transloco"
|
|
21492
22104
|
[description]="'listPanel.deleteDescription' | transloco: { key: 'listPanel.deleteShortcut' | transloco }"
|
|
21493
22105
|
(click)="onDelete(item, i, $event)">
|
|
21494
|
-
<
|
|
22106
|
+
<XMarkIcon />
|
|
21495
22107
|
</ListItemAction>
|
|
21496
22108
|
}
|
|
21497
22109
|
</ListItem>
|
|
@@ -21585,6 +22197,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
21585
22197
|
var ListPanelFooterComponent = class ListPanelFooterComponent {
|
|
21586
22198
|
cn = cn;
|
|
21587
22199
|
class = input(...ngDevMode ? [void 0, { debugName: "class" }] : /* istanbul ignore next */ []);
|
|
22200
|
+
keyboardAvailable = injectKeyboardAvailable();
|
|
21588
22201
|
hasMore = input.required(...ngDevMode ? [{ debugName: "hasMore" }] : /* istanbul ignore next */ []);
|
|
21589
22202
|
config = input.required(...ngDevMode ? [{ debugName: "config" }] : /* istanbul ignore next */ []);
|
|
21590
22203
|
loadMore = output();
|
|
@@ -21657,8 +22270,8 @@ var ListPanelFooterComponent = class ListPanelFooterComponent {
|
|
|
21657
22270
|
focus est un arrêt unique de tabulation, les flèches font le reste, et rien ne le dit.
|
|
21658
22271
|
aria-hidden : le listbox annonce déjà sa navigation aux lecteurs d'écran, et l'ouverture
|
|
21659
22272
|
d'une ligne est ce que tout le monde attend d'Entrée. La touche Suppr est, elle, écrite dans la
|
|
21660
|
-
ligne. -->
|
|
21661
|
-
@if (navigationHint()) {
|
|
22273
|
+
ligne. Absente quand la page se pilote au doigt (provideInputModality) : sans clavier, elle n'apprend rien. -->
|
|
22274
|
+
@if (navigationHint() && keyboardAvailable()) {
|
|
21662
22275
|
<p aria-hidden="true" class="flex min-w-0 flex-wrap items-center gap-x-(--spacing-xs) gap-y-(--spacing-4xs) text-(length:--size-sm) leading-(--line-height-2) text-(--font-neutral-base)">
|
|
21663
22276
|
<span class="inline-flex items-center gap-(--spacing-2xs)">
|
|
21664
22277
|
<!-- Des icônes et non les caractères ↑ ↓ : le centre visuel d'une flèche typographique ne tombe pas au
|
|
@@ -21770,8 +22383,8 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
21770
22383
|
focus est un arrêt unique de tabulation, les flèches font le reste, et rien ne le dit.
|
|
21771
22384
|
aria-hidden : le listbox annonce déjà sa navigation aux lecteurs d'écran, et l'ouverture
|
|
21772
22385
|
d'une ligne est ce que tout le monde attend d'Entrée. La touche Suppr est, elle, écrite dans la
|
|
21773
|
-
ligne. -->
|
|
21774
|
-
@if (navigationHint()) {
|
|
22386
|
+
ligne. Absente quand la page se pilote au doigt (provideInputModality) : sans clavier, elle n'apprend rien. -->
|
|
22387
|
+
@if (navigationHint() && keyboardAvailable()) {
|
|
21775
22388
|
<p aria-hidden="true" class="flex min-w-0 flex-wrap items-center gap-x-(--spacing-xs) gap-y-(--spacing-4xs) text-(length:--size-sm) leading-(--line-height-2) text-(--font-neutral-base)">
|
|
21776
22389
|
<span class="inline-flex items-center gap-(--spacing-2xs)">
|
|
21777
22390
|
<!-- Des icônes et non les caractères ↑ ↓ : le centre visuel d'une flèche typographique ne tombe pas au
|
|
@@ -26153,7 +26766,7 @@ var AggregationDateComponent = class AggregationDateComponent {
|
|
|
26153
26766
|
host: { properties: { "class": "cn(\"@container flex h-[inherit] max-h-[inherit] flex-col\", class(), isEmpty() && \"hidden\")" } },
|
|
26154
26767
|
providers: [provideTranslocoScope("filters")],
|
|
26155
26768
|
ngImport: i0,
|
|
26156
|
-
template: "<!-- `<details>`/`<summary>` stay native (open/close, the `name`-based exclusive accordion). A facet is a\n disclosure, not a grid row: see `aggregation-panel.html` for the why (same pattern, duplicated here\n since this facet type doesn't go through `AggregationPanelComponent`). The `<summary>` itself is\n `tabindex=\"-1\"`: the header's chevron is the keyboard stop, see the chevron below. Clear and apply are not in the\n header: they are in the body, in a bar at the top of the facet. -->\n<ng-template #content>\n <!-- content wrapper -->\n <!-- RGAA 11.5 / WCAG 1.3.1: the radios below all share one `name`, so they are one set\n and have to be exposed as one. `role=\"group\"` here rather than `role=\"radiogroup\"`\n on the list: a radiogroup owns its radios directly, and the <li> in between would be\n invalid owned children — the list would also stop announcing how many options it\n holds. The name is the aggregation's own title, the words the header already shows;\n with no title the group stays unnamed rather than carrying an empty label. -->\n @let groupName = aggregation()?.display;\n <form\n class=\"flex min-h-0 flex-1 flex-col\"\n (keydown.enter)=\"onOptionEnter($event)\">\n <!-- The same bar as for the list and the tree, in a popover and in the filters drawer alike: clear (a button with a text, there but disabled when there is nothing\n to clear) and, while a choice is pending, apply, without an icon. There is no search field here to put them under:\n they sit at the top of the facet. -->\n <div class=\"flex shrink-0 items-center justify-end gap-1 pt-1 pl-1\">\n <button\n type=\"button\"\n variant=\"tertiary\"\n size=\"
|
|
26769
|
+
template: "<!-- `<details>`/`<summary>` stay native (open/close, the `name`-based exclusive accordion). A facet is a\n disclosure, not a grid row: see `aggregation-panel.html` for the why (same pattern, duplicated here\n since this facet type doesn't go through `AggregationPanelComponent`). The `<summary>` itself is\n `tabindex=\"-1\"`: the header's chevron is the keyboard stop, see the chevron below. Clear and apply are not in the\n header: they are in the body, in a bar at the top of the facet. -->\n<ng-template #content>\n <!-- content wrapper -->\n <!-- RGAA 11.5 / WCAG 1.3.1: the radios below all share one `name`, so they are one set\n and have to be exposed as one. `role=\"group\"` here rather than `role=\"radiogroup\"`\n on the list: a radiogroup owns its radios directly, and the <li> in between would be\n invalid owned children — the list would also stop announcing how many options it\n holds. The name is the aggregation's own title, the words the header already shows;\n with no title the group stays unnamed rather than carrying an empty label. -->\n @let groupName = aggregation()?.display;\n <form\n class=\"flex min-h-0 flex-1 flex-col\"\n (keydown.enter)=\"onOptionEnter($event)\">\n <!-- The same bar as for the list and the tree, in a popover and in the filters drawer alike: clear (a button with a text, there but disabled when there is nothing\n to clear) and, while a choice is pending, apply, without an icon. There is no search field here to put them under:\n they sit at the top of the facet. -->\n <div class=\"flex shrink-0 items-center justify-end gap-1 pt-1 pl-1\">\n <button\n type=\"button\"\n variant=\"tertiary\"\n size=\"lg\"\n class=\"shrink-0\"\n [disabled]=\"!hasFilters() && !selection()\"\n [attr.aria-label]=\"'filters.clearFilters' | transloco\"\n (click)=\"clear()\">\n {{ \"filters.clearAll\" | transloco }}\n </button>\n @if (\n (!isControlled() || emitOn() === \"apply\") &&\n selection() &&\n validSelection()\n ) {\n <button\n type=\"button\"\n variant=\"accent\"\n size=\"lg\"\n class=\"shrink-0 overflow-hidden\"\n (click)=\"apply()\">\n <span class=\"truncate\">{{ \"filters.apply\" | transloco }}</span>\n </button>\n }\n </div>\n <div\n class=\"flex min-h-0 flex-1 flex-col\"\n role=\"group\"\n [attr.aria-label]=\"groupName ? (groupName | syslang | transloco) : null\">\n <ul\n class=\"flex min-h-0 flex-1 snap-y snap-start scrollbar-thin flex-col gap-0.5 overflow-auto px-1 pt-1 pb-3\"\n role=\"list\">\n @for (option of dateOptions(); track option.display) {\n <li\n role=\"listitem\"\n [attr.aria-label]=\"\n maskedLabel(option) ?? (option.display | syslang | transloco)\n \"\n [class]=\"\n cn(\n itemStateClass,\n 'flex min-h-9 cursor-pointer items-center px-2 py-0.5 text-base text-(--font-neutral-base) select-none [&>radio>label]:min-h-8',\n dateModel().option === option.display &&\n 'bg-(--bg-primary-lighter)',\n option.hidden && 'hidden',\n option.disabled && 'pointer-events-none opacity-40'\n )\n \"\n [attr.aria-hidden]=\"option.disabled\">\n <radio\n name=\"date-filter\"\n class=\"grow items-center [&_.radio-label]:text-base [&_.radio-label]:leading-6 [&_.radio-label]:font-normal\"\n [checked]=\"dateModel().option === option.display\"\n [disabled]=\"option.disabled\"\n (checkedChange)=\"selectOption(option.display)\">\n {{\n maskedLabel(option) ?? (option.display | syslang | transloco)\n }}\n </radio>\n </li>\n }\n\n @if (allowCustomRange) {\n <li\n role=\"listitem\"\n aria-label=\"open date range picker\"\n [class]=\"\n cn(\n itemStateClass,\n 'flex min-h-9 cursor-pointer items-center gap-(--spacing-xs) px-2 py-0.5 text-base text-(--font-neutral-base) select-none [&>radio>label]:min-h-8 [&>radio>label]:max-w-full [&>radio>label]:min-w-0',\n dateModel().option === 'custom-range' &&\n 'bg-(--bg-primary-lighter)'\n )\n \">\n <radio\n name=\"date-filter\"\n class=\"grow items-center [&_.radio-label]:text-base [&_.radio-label]:leading-6 [&_.radio-label]:font-normal\"\n [checked]=\"dateModel().option === 'custom-range'\"\n (checkedChange)=\"selectOption('custom-range')\">\n <!-- `<radio>` is `display: contents`: its `<label>` is a child of the `<li>` in the box tree but not in the DOM, hence\n the `[&>radio>label]` of the row above. Every box of the chain down to the inputs may shrink (`min-w-0`, no fixed `min-w-[18ch]`): in the 15.5rem popover the\n label, the field and the radio do not fit side by side, and a box that cannot shrink spills out of it and is clipped.\n The fields have a definite width (`w-[18ch]`) that may shrink (`max-w-full min-w-0`). On a touch screen the text is 16px\n instead of 14px, which is what made the row wider than the 15.5rem popover. -->\n <div class=\"flex min-w-0 flex-wrap items-center gap-x-1 gap-y-1\">\n <div class=\"flex min-w-0 items-center gap-1\">\n <label\n [attr.for]=\"startId\"\n class=\"radio-label min-w-8 shrink-0 truncate\"\n >{{ \"filters.from\" | transloco }}</label\n >\n <input-group\n class=\"h-8 w-[18ch] max-w-full min-w-0 has-[[readonly]]:border-(--stroke-primary-muted)\">\n <input\n input-control\n [id]=\"startId\"\n name=\"start\"\n type=\"text\"\n readonly\n [value]=\"customRangeFrom()\"\n (click)=\"selectAndOpenDialog()\"\n (keydown.enter)=\"onRangeInputEnter($event)\" />\n </input-group>\n </div>\n <div class=\"flex min-w-0 items-center gap-1\">\n <label\n [attr.for]=\"endId\"\n class=\"radio-label min-w-8 shrink-0 truncate text-right\"\n >{{ \"filters.to\" | transloco }}</label\n >\n <input-group\n class=\"h-8 w-[18ch] max-w-full min-w-0 has-[[readonly]]:border-(--stroke-primary-muted)\">\n <input\n input-control\n [id]=\"endId\"\n name=\"end\"\n type=\"text\"\n readonly\n [value]=\"customRangeTo()\"\n (click)=\"selectAndOpenDialog()\"\n (keydown.enter)=\"onRangeInputEnter($event)\" />\n </input-group>\n </div>\n </div>\n </radio>\n </li>\n }\n </ul>\n </div>\n </form>\n</ng-template>\n\n@if (headless()) {\n <div class=\"flex min-h-0 flex-1 flex-col\">\n <ng-container [ngTemplateOutlet]=\"content\" />\n </div>\n} @else {\n <details\n [attr.open]=\"expanded()\"\n [attr.name]=\"id()\"\n class=\"group flex min-h-0 flex-1 flex-col space-y-2 p-0.5 details-content:flex details-content:min-h-0 details-content:flex-col\">\n <!-- `p-0.5` on the details: the one of AggregationPanel (the list and the tree), so the header and its chevron sit on the same\n lines as theirs in the aside filters and the filters panel — without it the chevron was 2px further right.\n scroll-mb-3/scroll-mt-16: see aggregation-panel-row.html for the full rationale\n (bottom margin against the container's edge, much bigger top margin to also clear\n <More>'s sticky facet-name search box when navigating ArrowUp). -->\n <summary\n tabindex=\"-1\"\n [class.cursor-pointer]=\"collapsible()\"\n [class]=\"rowStateClass\"\n class=\"m-0 flex h-8 w-full shrink-0 scroll-mt-16 scroll-mb-3 items-center gap-1 pl-1 font-semibold select-none\"\n (click)=\"onHeaderClick($event)\">\n <ng-content select=\"label\">\n @let icon = aggregation()?.icon;\n @if (icon) {\n <FaIcon [faClass]=\"icon\" class=\"mr-1 shrink-0\" />\n }\n <span class=\"grow\">{{\n aggregation()?.display | syslang | transloco\n }}</span>\n </ng-content>\n\n @if (collapsible()) {\n <!-- The `<summary>` is out of the tab order (`tabindex=\"-1\"`), so this button is the keyboard way to\n collapse the facet. Its click bubbles to the `<summary>`, whose handler does the toggle, and it\n exposes `aria-expanded`. -->\n <button\n type=\"button\"\n variant=\"tertiary\"\n [iconOnly]=\"true\"\n size=\"md\"\n [aria-label]=\"'filters.toggle' | transloco\"\n [attr.aria-expanded]=\"!isCollapsed()\"\n [tooltip]=\"'filters.toggle' | transloco\"\n class=\"shrink-0 cursor-pointer [&_svg]:transition-transform [&_svg]:duration-150 group-open:[&_svg]:rotate-90\">\n <chevron-right class=\"shrink-0\" />\n </button>\n }\n </summary>\n <ng-container [ngTemplateOutlet]=\"content\" />\n </details>\n}\n",
|
|
26157
26770
|
styles: [":host{interpolate-size:allow-keywords}details::details-content{flex:1 1 auto;block-size:0;overflow:clip;overflow-clip-margin:.25rem;transition:block-size .2s cubic-bezier(.32,.72,0,1),content-visibility .2s allow-discrete}details[open]::details-content{block-size:auto}@media(prefers-reduced-motion:reduce){details::details-content{transition:none}}\n", ":host{min-width:200px}ul[role=list]{scrollbar-width:thin}\n"],
|
|
26158
26771
|
dependencies: [
|
|
26159
26772
|
{
|
|
@@ -26285,7 +26898,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
26285
26898
|
TooltipDirective
|
|
26286
26899
|
],
|
|
26287
26900
|
host: { "[class]": "cn(\"@container flex h-[inherit] max-h-[inherit] flex-col\", class(), isEmpty() && \"hidden\")" },
|
|
26288
|
-
template: "<!-- `<details>`/`<summary>` stay native (open/close, the `name`-based exclusive accordion). A facet is a\n disclosure, not a grid row: see `aggregation-panel.html` for the why (same pattern, duplicated here\n since this facet type doesn't go through `AggregationPanelComponent`). The `<summary>` itself is\n `tabindex=\"-1\"`: the header's chevron is the keyboard stop, see the chevron below. Clear and apply are not in the\n header: they are in the body, in a bar at the top of the facet. -->\n<ng-template #content>\n <!-- content wrapper -->\n <!-- RGAA 11.5 / WCAG 1.3.1: the radios below all share one `name`, so they are one set\n and have to be exposed as one. `role=\"group\"` here rather than `role=\"radiogroup\"`\n on the list: a radiogroup owns its radios directly, and the <li> in between would be\n invalid owned children — the list would also stop announcing how many options it\n holds. The name is the aggregation's own title, the words the header already shows;\n with no title the group stays unnamed rather than carrying an empty label. -->\n @let groupName = aggregation()?.display;\n <form\n class=\"flex min-h-0 flex-1 flex-col\"\n (keydown.enter)=\"onOptionEnter($event)\">\n <!-- The same bar as for the list and the tree, in a popover and in the filters drawer alike: clear (a button with a text, there but disabled when there is nothing\n to clear) and, while a choice is pending, apply, without an icon. There is no search field here to put them under:\n they sit at the top of the facet. -->\n <div class=\"flex shrink-0 items-center justify-end gap-1 pt-1 pl-1\">\n <button\n type=\"button\"\n variant=\"tertiary\"\n size=\"
|
|
26901
|
+
template: "<!-- `<details>`/`<summary>` stay native (open/close, the `name`-based exclusive accordion). A facet is a\n disclosure, not a grid row: see `aggregation-panel.html` for the why (same pattern, duplicated here\n since this facet type doesn't go through `AggregationPanelComponent`). The `<summary>` itself is\n `tabindex=\"-1\"`: the header's chevron is the keyboard stop, see the chevron below. Clear and apply are not in the\n header: they are in the body, in a bar at the top of the facet. -->\n<ng-template #content>\n <!-- content wrapper -->\n <!-- RGAA 11.5 / WCAG 1.3.1: the radios below all share one `name`, so they are one set\n and have to be exposed as one. `role=\"group\"` here rather than `role=\"radiogroup\"`\n on the list: a radiogroup owns its radios directly, and the <li> in between would be\n invalid owned children — the list would also stop announcing how many options it\n holds. The name is the aggregation's own title, the words the header already shows;\n with no title the group stays unnamed rather than carrying an empty label. -->\n @let groupName = aggregation()?.display;\n <form\n class=\"flex min-h-0 flex-1 flex-col\"\n (keydown.enter)=\"onOptionEnter($event)\">\n <!-- The same bar as for the list and the tree, in a popover and in the filters drawer alike: clear (a button with a text, there but disabled when there is nothing\n to clear) and, while a choice is pending, apply, without an icon. There is no search field here to put them under:\n they sit at the top of the facet. -->\n <div class=\"flex shrink-0 items-center justify-end gap-1 pt-1 pl-1\">\n <button\n type=\"button\"\n variant=\"tertiary\"\n size=\"lg\"\n class=\"shrink-0\"\n [disabled]=\"!hasFilters() && !selection()\"\n [attr.aria-label]=\"'filters.clearFilters' | transloco\"\n (click)=\"clear()\">\n {{ \"filters.clearAll\" | transloco }}\n </button>\n @if (\n (!isControlled() || emitOn() === \"apply\") &&\n selection() &&\n validSelection()\n ) {\n <button\n type=\"button\"\n variant=\"accent\"\n size=\"lg\"\n class=\"shrink-0 overflow-hidden\"\n (click)=\"apply()\">\n <span class=\"truncate\">{{ \"filters.apply\" | transloco }}</span>\n </button>\n }\n </div>\n <div\n class=\"flex min-h-0 flex-1 flex-col\"\n role=\"group\"\n [attr.aria-label]=\"groupName ? (groupName | syslang | transloco) : null\">\n <ul\n class=\"flex min-h-0 flex-1 snap-y snap-start scrollbar-thin flex-col gap-0.5 overflow-auto px-1 pt-1 pb-3\"\n role=\"list\">\n @for (option of dateOptions(); track option.display) {\n <li\n role=\"listitem\"\n [attr.aria-label]=\"\n maskedLabel(option) ?? (option.display | syslang | transloco)\n \"\n [class]=\"\n cn(\n itemStateClass,\n 'flex min-h-9 cursor-pointer items-center px-2 py-0.5 text-base text-(--font-neutral-base) select-none [&>radio>label]:min-h-8',\n dateModel().option === option.display &&\n 'bg-(--bg-primary-lighter)',\n option.hidden && 'hidden',\n option.disabled && 'pointer-events-none opacity-40'\n )\n \"\n [attr.aria-hidden]=\"option.disabled\">\n <radio\n name=\"date-filter\"\n class=\"grow items-center [&_.radio-label]:text-base [&_.radio-label]:leading-6 [&_.radio-label]:font-normal\"\n [checked]=\"dateModel().option === option.display\"\n [disabled]=\"option.disabled\"\n (checkedChange)=\"selectOption(option.display)\">\n {{\n maskedLabel(option) ?? (option.display | syslang | transloco)\n }}\n </radio>\n </li>\n }\n\n @if (allowCustomRange) {\n <li\n role=\"listitem\"\n aria-label=\"open date range picker\"\n [class]=\"\n cn(\n itemStateClass,\n 'flex min-h-9 cursor-pointer items-center gap-(--spacing-xs) px-2 py-0.5 text-base text-(--font-neutral-base) select-none [&>radio>label]:min-h-8 [&>radio>label]:max-w-full [&>radio>label]:min-w-0',\n dateModel().option === 'custom-range' &&\n 'bg-(--bg-primary-lighter)'\n )\n \">\n <radio\n name=\"date-filter\"\n class=\"grow items-center [&_.radio-label]:text-base [&_.radio-label]:leading-6 [&_.radio-label]:font-normal\"\n [checked]=\"dateModel().option === 'custom-range'\"\n (checkedChange)=\"selectOption('custom-range')\">\n <!-- `<radio>` is `display: contents`: its `<label>` is a child of the `<li>` in the box tree but not in the DOM, hence\n the `[&>radio>label]` of the row above. Every box of the chain down to the inputs may shrink (`min-w-0`, no fixed `min-w-[18ch]`): in the 15.5rem popover the\n label, the field and the radio do not fit side by side, and a box that cannot shrink spills out of it and is clipped.\n The fields have a definite width (`w-[18ch]`) that may shrink (`max-w-full min-w-0`). On a touch screen the text is 16px\n instead of 14px, which is what made the row wider than the 15.5rem popover. -->\n <div class=\"flex min-w-0 flex-wrap items-center gap-x-1 gap-y-1\">\n <div class=\"flex min-w-0 items-center gap-1\">\n <label\n [attr.for]=\"startId\"\n class=\"radio-label min-w-8 shrink-0 truncate\"\n >{{ \"filters.from\" | transloco }}</label\n >\n <input-group\n class=\"h-8 w-[18ch] max-w-full min-w-0 has-[[readonly]]:border-(--stroke-primary-muted)\">\n <input\n input-control\n [id]=\"startId\"\n name=\"start\"\n type=\"text\"\n readonly\n [value]=\"customRangeFrom()\"\n (click)=\"selectAndOpenDialog()\"\n (keydown.enter)=\"onRangeInputEnter($event)\" />\n </input-group>\n </div>\n <div class=\"flex min-w-0 items-center gap-1\">\n <label\n [attr.for]=\"endId\"\n class=\"radio-label min-w-8 shrink-0 truncate text-right\"\n >{{ \"filters.to\" | transloco }}</label\n >\n <input-group\n class=\"h-8 w-[18ch] max-w-full min-w-0 has-[[readonly]]:border-(--stroke-primary-muted)\">\n <input\n input-control\n [id]=\"endId\"\n name=\"end\"\n type=\"text\"\n readonly\n [value]=\"customRangeTo()\"\n (click)=\"selectAndOpenDialog()\"\n (keydown.enter)=\"onRangeInputEnter($event)\" />\n </input-group>\n </div>\n </div>\n </radio>\n </li>\n }\n </ul>\n </div>\n </form>\n</ng-template>\n\n@if (headless()) {\n <div class=\"flex min-h-0 flex-1 flex-col\">\n <ng-container [ngTemplateOutlet]=\"content\" />\n </div>\n} @else {\n <details\n [attr.open]=\"expanded()\"\n [attr.name]=\"id()\"\n class=\"group flex min-h-0 flex-1 flex-col space-y-2 p-0.5 details-content:flex details-content:min-h-0 details-content:flex-col\">\n <!-- `p-0.5` on the details: the one of AggregationPanel (the list and the tree), so the header and its chevron sit on the same\n lines as theirs in the aside filters and the filters panel — without it the chevron was 2px further right.\n scroll-mb-3/scroll-mt-16: see aggregation-panel-row.html for the full rationale\n (bottom margin against the container's edge, much bigger top margin to also clear\n <More>'s sticky facet-name search box when navigating ArrowUp). -->\n <summary\n tabindex=\"-1\"\n [class.cursor-pointer]=\"collapsible()\"\n [class]=\"rowStateClass\"\n class=\"m-0 flex h-8 w-full shrink-0 scroll-mt-16 scroll-mb-3 items-center gap-1 pl-1 font-semibold select-none\"\n (click)=\"onHeaderClick($event)\">\n <ng-content select=\"label\">\n @let icon = aggregation()?.icon;\n @if (icon) {\n <FaIcon [faClass]=\"icon\" class=\"mr-1 shrink-0\" />\n }\n <span class=\"grow\">{{\n aggregation()?.display | syslang | transloco\n }}</span>\n </ng-content>\n\n @if (collapsible()) {\n <!-- The `<summary>` is out of the tab order (`tabindex=\"-1\"`), so this button is the keyboard way to\n collapse the facet. Its click bubbles to the `<summary>`, whose handler does the toggle, and it\n exposes `aria-expanded`. -->\n <button\n type=\"button\"\n variant=\"tertiary\"\n [iconOnly]=\"true\"\n size=\"md\"\n [aria-label]=\"'filters.toggle' | transloco\"\n [attr.aria-expanded]=\"!isCollapsed()\"\n [tooltip]=\"'filters.toggle' | transloco\"\n class=\"shrink-0 cursor-pointer [&_svg]:transition-transform [&_svg]:duration-150 group-open:[&_svg]:rotate-90\">\n <chevron-right class=\"shrink-0\" />\n </button>\n }\n </summary>\n <ng-container [ngTemplateOutlet]=\"content\" />\n </details>\n}\n",
|
|
26289
26902
|
styles: [":host{interpolate-size:allow-keywords}details::details-content{flex:1 1 auto;block-size:0;overflow:clip;overflow-clip-margin:.25rem;transition:block-size .2s cubic-bezier(.32,.72,0,1),content-visibility .2s allow-discrete}details[open]::details-content{block-size:auto}@media(prefers-reduced-motion:reduce){details::details-content{transition:none}}\n", ":host{min-width:200px}ul[role=list]{scrollbar-width:thin}\n"]
|
|
26290
26903
|
}]
|
|
26291
26904
|
}],
|
|
@@ -26821,7 +27434,7 @@ var AggregationPanelComponent = class AggregationPanelComponent {
|
|
|
26821
27434
|
isSignal: true
|
|
26822
27435
|
}],
|
|
26823
27436
|
ngImport: i0,
|
|
26824
|
-
template: "<!-- `<details>`/`<summary>` stay native (open/close, the `name`-based exclusive accordion): a facet is\n a disclosure, not a grid row. They used to sit in an `@angular/aria` grid so ArrowUp/ArrowDown could\n jump from one facet header to the next, but a grid without headers reads as a layout table\n (RGAA 5.3). The `<summary>` itself is `tabindex=\"-1\"`: Tab walks the header's chevron, never the header row, and the\n chevron is the keyboard way to collapse. Select all, clear and apply are not in the header: they are in the body, in a\n bar under the search field. Label, header actions (the count and the chevron) and body are `<ng-template>`s: `<ng-content select=\"…\">` can only be\n declared once, and the body is projected content. -->\n\n<ng-template #searchToggleSlot>\n <button\n #searchToggle\n type=\"button\"\n variant=\"tertiary\"\n [iconOnly]=\"true\"\n size=\"sm\"\n class=\"shrink-0 cursor-pointer\"\n [attr.aria-expanded]=\"showSearch()\"\n [attr.aria-controls]=\"searchId\"\n [tooltip]=\"\n 'filters.searchInFacet'\n | transloco: { facet: (aggregation()?.display | syslang | transloco) }\n \"\n (click)=\"toggleSearch($event)\">\n <search-icon class=\"shrink-0\" />\n <span class=\"sr-only\">{{\n \"filters.searchInFacet\"\n | transloco: { facet: (aggregation()?.display | syslang | transloco) }\n }}</span>\n </button>\n</ng-template>\n\n<ng-template #labelSlot>\n <ng-content select=\"label\">\n @let icon = aggregation()?.icon;\n @if (icon) {\n <FaIcon [faClass]=\"icon\" class=\"mr-1 shrink-0\" />\n }\n <span class=\"grow truncate\">{{\n aggregation()?.display | syslang | transloco\n }}</span>\n </ng-content>\n</ng-template>\n\n<ng-template #headerActionsSlot>\n @if (showFiltersCount() && filtersCount() > 0) {\n <Badge variant=\"number\" fill=\"primary\" scheme=\"sage\" size=\"xs\" class=\"ml-1\">\n {{ filtersCount() }}\n </Badge>\n }\n @if (canSearch() && !isCollapsed()) {\n <!-- The magnifier opens the search field of the facet (and closes it): the field is not on screen until then. It is\n in the header, next to the chevron, and only while the facet is open — there is nothing to search in a closed\n one. Interactive content in a summary does not toggle the details by itself, and the click is stopped anyway. -->\n <ng-container [ngTemplateOutlet]=\"searchToggleSlot\" />\n }\n @if (collapsible()) {\n <!-- The `<summary>` is out of the tab order (`tabindex=\"-1\"`), so this button is the keyboard way to\n collapse the facet. Its click bubbles to the `<summary>`, whose handler does the toggle, and it\n exposes `aria-expanded`. -->\n <button\n type=\"button\"\n variant=\"tertiary\"\n [iconOnly]=\"true\"\n size=\"sm\"\n [attr.aria-expanded]=\"!isCollapsed()\"\n [tooltip]=\"'filters.toggle' | transloco\"\n class=\"shrink-0 cursor-pointer [&_svg]:transition-transform [&_svg]:duration-150 group-open:[&_svg]:rotate-90\">\n <chevron-right class=\"shrink-0\" />\n <span class=\"sr-only\">{{ \"filters.toggle\" | transloco }}</span>\n </button>\n }\n</ng-template>\n\n<ng-template #bodySlot>\n <div class=\"flex min-h-0 flex-1 flex-col\">\n @if (canSearch()) {\n <!-- The field is always in the DOM and folded to nothing until it is asked for: a grid row that goes from 0fr to 1fr is\n the one height animation that needs no measuring. `inert` takes the folded field out of the tab order and of the\n accessibility tree. The margins live in the inner box: a padding on the folded element itself would keep it\n from reaching zero.\n\n No `hotkey`: this field is one of many facet search boxes on the page, so it must not advertise a global shortcut.\n A placeholder is not a label (RGAA 11.1): the name says WHICH facet this box filters — the one thing that tells a\n dozen identical fields apart. SearchInput hands its class to its inner field, so a margin on it would count twice,\n which is one more reason for the padding on the box around it. -->\n <div\n [id]=\"searchId\"\n class=\"grid shrink-0 transition-[grid-template-rows,opacity] duration-200 ease-[cubic-bezier(0.32,0.72,0,1)] motion-reduce:transition-none\"\n [class]=\"\n showSearch()\n ? 'grid-rows-[1fr] opacity-100'\n : 'grid-rows-[0fr] opacity-0'\n \"\n [inert]=\"!showSearch()\">\n <!-- `-mx-1 px-1`: the box clips what is outside it, and the focus ring of the field is drawn outside the field,\n so it needs a few pixels of room on each side — taken back by the negative margin so nothing moves. -->\n <div class=\"-mx-1 min-h-0 overflow-hidden px-1\">\n <div [class]=\"headless() ? 'pt-0.5 pb-1' : 'pt-2 pb-1'\">\n <SearchInput\n [placeholder]=\"'search' | transloco\"\n [clearLabel]=\"'filters.clearSearch' | transloco\"\n [aria-label]=\"\n 'filters.searchInFacet'\n | transloco\n : { facet: (aggregation()?.display | syslang | transloco) }\n \"\n [(value)]=\"searchText\"\n (keydown.escape)=\"onSearchEscape($event)\">\n <ng-content select=\"[search-addon]\" />\n </SearchInput>\n </div>\n </div>\n </div>\n }\n\n @if (itemsLength() > 0 || hasFilters() || selection()) {\n <!-- The bar under the search field, in a popover and in the filters drawer alike: clear and apply, and nothing else, so\n both keep their full text in the little room there is. Clear is a button with a text, not an icon, and stays in\n place (disabled) when there is nothing to clear. Apply only shows once something is pending, as it always did, and\n sits at the same level. -->\n <div\n data-bar=\"actions\"\n class=\"mb-1 flex shrink-0 items-center justify-end gap-1\">\n @if (headless() && canSearch()) {\n <ng-container [ngTemplateOutlet]=\"searchToggleSlot\" />\n }\n <button\n type=\"button\"\n variant=\"tertiary\"\n size=\"sm\"\n class=\"shrink-0\"\n [disabled]=\"!hasFilters() && selectedCount() === 0\"\n [attr.aria-label]=\"'filters.clearFilters' | transloco\"\n (click)=\"cleared.emit()\">\n {{ \"filters.clearAll\" | transloco }}\n </button>\n @if (selection()) {\n <!-- No icon: the room is short, and the word says it. -->\n <button\n type=\"button\"\n variant=\"accent\"\n size=\"sm\"\n class=\"shrink-0 overflow-hidden\"\n (click)=\"applied.emit()\">\n <span class=\"truncate\">{{ \"filters.apply\" | transloco }}</span>\n </button>\n }\n </div>\n }\n\n @if (itemsLength() > 0) {\n <!-- Select all is the first row of the list, written like one: the box lines up with the boxes of the items it drives and\n carries its own text, which a bare box next to two buttons never did. The text lines up with the items' too: same gap\n after the box (`gap-2`) and same size (`text-sm`, where the checkbox's own label would be 12px). The whole row is the label, so it is as easy\n to hit as an item. The count of checked items is part of that same label (\"Select all (3)\"), so the two can never\n overlap: when the room is short the label is cut with an ellipsis as a whole. It stays out of the scroller of the list, so it does not scroll away with the items. -->\n <div\n data-bar=\"select-all\"\n class=\"mb-1 flex shrink-0 items-center pr-3 pl-1.5 text-sm\">\n <checkbox\n class=\"min-w-0 flex-1 items-center gap-2 py-1 [&_.checkbox-label]:truncate [&_.checkbox-label]:text-sm [&_.checkbox-label]:leading-6 [&_.checkbox-text-block]:min-w-0\"\n [checked]=\"isAllSelected()\"\n [indeterminate]=\"selectedCount() > 0 && !isAllSelected()\"\n (checkedChange)=\"$event ? allSelected.emit() : allUnselected.emit()\">\n {{\n (isAllSelected() ? \"filters.unselectAll\" : \"filters.selectAll\")\n | transloco\n }}\n @if (selectedCount() > 0) {\n <span class=\"text-(--font-primary-muted)\"\n >({{ selectedCount() }})</span\n >\n }\n </checkbox>\n <span class=\"sr-only\" aria-live=\"polite\">{{\n \"filters.selectedCount\" | transloco: { count: selectedCount() }\n }}</span>\n </div>\n }\n\n <ng-content />\n\n @if (hasMore() && searchedItemsLength() === 0) {\n <!-- The placeholder rows of a loading page are not here: they are rows of the scrolling area, in the list and the tree\n (AggregationLoadMoreSkeleton). Here, between the list and the button, they would shorten the list. -->\n <!-- `variant=\"none\"`: galactik's ButtonComponent matches every <button> and would otherwise dress this one as a primary\n button (filled, rounded, 36px high). `none` leaves the classes below as the only style. -->\n <!-- `aria-disabled`, not `disabled`: the focus stays on the button while the page loads. The accessible name stays\n \"load more\" and `aria-busy` tells that it is working, so no extra text has to be translated by the application. -->\n <button\n type=\"button\"\n data-load-more=\"button\"\n variant=\"none\"\n class=\"mt-1 flex w-full shrink-0 cursor-pointer items-center justify-center rounded-(--radius-xs) px-2 py-1.5 text-(--font-primary-muted) underline underline-offset-4 hover:bg-(--bg-primary-lighter) hover:text-(--font-primary-base) focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-(--stroke-focus) focus-visible:outline-solid aria-busy:cursor-progress aria-busy:no-underline aria-busy:hover:bg-transparent\"\n [attr.aria-label]=\"'loadMore' | transloco\"\n [attr.aria-busy]=\"loading() ? 'true' : null\"\n [attr.aria-disabled]=\"loading() ? 'true' : null\"\n (click)=\"requestMore()\">\n <!-- Both states share one grid cell: the button keeps its width and height when the text gives way to the dots. -->\n <span class=\"inline-grid\">\n <span class=\"col-start-1 row-start-1\" [class.invisible]=\"loading()\">\n {{ \"loadMore\" | transloco }}\n </span>\n @if (loading()) {\n <loading-dots size=\"sm\" class=\"col-start-1 row-start-1\" />\n }\n </span>\n </button>\n }\n </div>\n</ng-template>\n\n@if (headless()) {\n <div class=\"flex min-h-0 flex-1 flex-col p-0.5\">\n <ng-container [ngTemplateOutlet]=\"bodySlot\" />\n </div>\n} @else {\n <details\n [attr.open]=\"expanded()\"\n [attr.name]=\"id()\"\n class=\"group flex min-h-0 flex-1 flex-col p-0.5 details-content:flex details-content:min-h-0 details-content:flex-col\"\n (toggle)=\"onToggle($event)\">\n <summary\n tabindex=\"-1\"\n [class.cursor-pointer]=\"collapsible() && !isEmpty()\"\n [class]=\"\n (isEmpty() ? 'text-(--font-neutral-muted) ' : '') + rowStateClass\n \"\n class=\"m-0 flex h-8 w-full shrink-0 scroll-mt-16 scroll-mb-3 items-center gap-1 pl-1 font-semibold select-none\"\n (click)=\"onHeaderClick($event)\">\n <ng-container [ngTemplateOutlet]=\"labelSlot\" />\n <ng-container [ngTemplateOutlet]=\"headerActionsSlot\" />\n </summary>\n <ng-container [ngTemplateOutlet]=\"bodySlot\" />\n </details>\n}\n",
|
|
27437
|
+
template: "<!-- `<details>`/`<summary>` stay native (open/close, the `name`-based exclusive accordion): a facet is\n a disclosure, not a grid row. They used to sit in an `@angular/aria` grid so ArrowUp/ArrowDown could\n jump from one facet header to the next, but a grid without headers reads as a layout table\n (RGAA 5.3). The `<summary>` itself is `tabindex=\"-1\"`: Tab walks the header's chevron, never the header row, and the\n chevron is the keyboard way to collapse. Select all, clear and apply are not in the header: they are in the body, in a\n bar under the search field. Label, header actions (the count and the chevron) and body are `<ng-template>`s: `<ng-content select=\"…\">` can only be\n declared once, and the body is projected content. -->\n\n<ng-template #searchToggleSlot>\n <button\n #searchToggle\n type=\"button\"\n variant=\"tertiary\"\n [iconOnly]=\"true\"\n size=\"md\"\n class=\"shrink-0 cursor-pointer\"\n [attr.aria-expanded]=\"showSearch()\"\n [attr.aria-controls]=\"searchId\"\n [tooltip]=\"\n 'filters.searchInFacet'\n | transloco: { facet: (aggregation()?.display | syslang | transloco) }\n \"\n (click)=\"toggleSearch($event)\">\n <search-icon class=\"shrink-0\" />\n <span class=\"sr-only\">{{\n \"filters.searchInFacet\"\n | transloco: { facet: (aggregation()?.display | syslang | transloco) }\n }}</span>\n </button>\n</ng-template>\n\n<ng-template #labelSlot>\n <ng-content select=\"label\">\n @let icon = aggregation()?.icon;\n @if (icon) {\n <FaIcon [faClass]=\"icon\" class=\"mr-1 shrink-0\" />\n }\n <span class=\"grow truncate\">{{\n aggregation()?.display | syslang | transloco\n }}</span>\n </ng-content>\n</ng-template>\n\n<ng-template #headerActionsSlot>\n @if (showFiltersCount() && filtersCount() > 0) {\n <Badge variant=\"number\" fill=\"primary\" scheme=\"sage\" size=\"xs\" class=\"ml-1\">\n {{ filtersCount() }}\n </Badge>\n }\n @if (canSearch() && !isCollapsed()) {\n <!-- The magnifier opens the search field of the facet (and closes it): the field is not on screen until then. It is\n in the header, next to the chevron, and only while the facet is open — there is nothing to search in a closed\n one. Interactive content in a summary does not toggle the details by itself, and the click is stopped anyway. -->\n <ng-container [ngTemplateOutlet]=\"searchToggleSlot\" />\n }\n @if (collapsible()) {\n <!-- The `<summary>` is out of the tab order (`tabindex=\"-1\"`), so this button is the keyboard way to\n collapse the facet. Its click bubbles to the `<summary>`, whose handler does the toggle, and it\n exposes `aria-expanded`. -->\n <button\n type=\"button\"\n variant=\"tertiary\"\n [iconOnly]=\"true\"\n size=\"md\"\n [attr.aria-expanded]=\"!isCollapsed()\"\n [tooltip]=\"'filters.toggle' | transloco\"\n class=\"shrink-0 cursor-pointer [&_svg]:transition-transform [&_svg]:duration-150 group-open:[&_svg]:rotate-90\">\n <chevron-right class=\"shrink-0\" />\n <span class=\"sr-only\">{{ \"filters.toggle\" | transloco }}</span>\n </button>\n }\n</ng-template>\n\n<ng-template #bodySlot>\n <div class=\"flex min-h-0 flex-1 flex-col\">\n @if (canSearch()) {\n <!-- The field is always in the DOM and folded to nothing until it is asked for: a grid row that goes from 0fr to 1fr is\n the one height animation that needs no measuring. `inert` takes the folded field out of the tab order and of the\n accessibility tree. The margins live in the inner box: a padding on the folded element itself would keep it\n from reaching zero.\n\n No `hotkey`: this field is one of many facet search boxes on the page, so it must not advertise a global shortcut.\n A placeholder is not a label (RGAA 11.1): the name says WHICH facet this box filters — the one thing that tells a\n dozen identical fields apart. SearchInput hands its class to its inner field, so a margin on it would count twice,\n which is one more reason for the padding on the box around it. -->\n <div\n [id]=\"searchId\"\n class=\"grid shrink-0 transition-[grid-template-rows,opacity] duration-200 ease-[cubic-bezier(0.32,0.72,0,1)] motion-reduce:transition-none\"\n [class]=\"\n showSearch()\n ? 'grid-rows-[1fr] opacity-100'\n : 'grid-rows-[0fr] opacity-0'\n \"\n [inert]=\"!showSearch()\">\n <!-- `px-0.5`: the box clips what is outside it, and the focus ring of the field is drawn outside the field (1px past its\n border), so it needs a little room on each side. No negative margin to take it back: it pushed that room OUTSIDE\n the box, into the clipping of the popover or the drawer around, and the ring was cut on both sides. -->\n <div class=\"min-h-0 overflow-hidden px-0.5\">\n <div [class]=\"headless() ? 'pt-0.5 pb-1' : 'pt-2 pb-1'\">\n <SearchInput\n [placeholder]=\"'search' | transloco\"\n [clearLabel]=\"'filters.clearSearch' | transloco\"\n [aria-label]=\"\n 'filters.searchInFacet'\n | transloco\n : { facet: (aggregation()?.display | syslang | transloco) }\n \"\n [(value)]=\"searchText\"\n (keydown.escape)=\"onSearchEscape($event)\">\n <ng-content select=\"[search-addon]\" />\n </SearchInput>\n </div>\n </div>\n </div>\n }\n\n @if (itemsLength() > 0 || hasFilters() || selection()) {\n <!-- The bar under the search field, in a popover and in the filters drawer alike: clear and apply, and nothing else, so\n both keep their full text in the little room there is. Clear is a button with a text, not an icon, and stays in\n place (disabled) when there is nothing to clear. Apply only shows once something is pending, as it always did, and\n sits at the same level. -->\n <div\n data-bar=\"actions\"\n class=\"mb-1 flex shrink-0 items-center justify-end gap-1\">\n @if (headless() && canSearch()) {\n <ng-container [ngTemplateOutlet]=\"searchToggleSlot\" />\n }\n <button\n type=\"button\"\n variant=\"tertiary\"\n size=\"lg\"\n class=\"shrink-0\"\n [disabled]=\"!hasFilters() && selectedCount() === 0\"\n [attr.aria-label]=\"'filters.clearFilters' | transloco\"\n (click)=\"cleared.emit()\">\n {{ \"filters.clearAll\" | transloco }}\n </button>\n @if (selection()) {\n <!-- No icon: the room is short, and the word says it. -->\n <button\n type=\"button\"\n variant=\"accent\"\n size=\"lg\"\n class=\"shrink-0 overflow-hidden\"\n (click)=\"applied.emit()\">\n <span class=\"truncate\">{{ \"filters.apply\" | transloco }}</span>\n </button>\n }\n </div>\n }\n\n @if (itemsLength() > 0) {\n <!-- Select all is the first row of the list, written like one: the box lines up with the boxes of the items it drives and\n carries its own text, which a bare box next to two buttons never did. The text lines up with the items' too: same gap\n after the box (`gap-2`) and same size (`text-sm`, where the checkbox's own label would be 12px). The whole row is the label, so it is as easy\n to hit as an item. The count of checked items is part of that same label (\"Select all (3)\"), so the two can never\n overlap: when the room is short the label is cut with an ellipsis as a whole. It stays out of the scroller of the list, so it does not scroll away with the items. -->\n <div\n data-bar=\"select-all\"\n class=\"mb-1 flex shrink-0 items-center pr-3 pl-1.5 text-sm\">\n <checkbox\n class=\"min-w-0 flex-1 items-center gap-2 py-1.5 [&_.checkbox-label]:truncate [&_.checkbox-label]:text-base [&_.checkbox-label]:leading-6 [&_.checkbox-text-block]:min-w-0\"\n [checked]=\"isAllSelected()\"\n [indeterminate]=\"selectedCount() > 0 && !isAllSelected()\"\n (checkedChange)=\"$event ? allSelected.emit() : allUnselected.emit()\">\n {{\n (isAllSelected() ? \"filters.unselectAll\" : \"filters.selectAll\")\n | transloco\n }}\n @if (selectedCount() > 0) {\n <span class=\"text-(--font-primary-muted)\"\n >({{ selectedCount() }})</span\n >\n }\n </checkbox>\n <span class=\"sr-only\" aria-live=\"polite\">{{\n \"filters.selectedCount\" | transloco: { count: selectedCount() }\n }}</span>\n </div>\n }\n\n <ng-content />\n\n @if (hasMore() && searchedItemsLength() === 0) {\n <!-- The placeholder rows of a loading page are not here: they are rows of the scrolling area, in the list and the tree\n (AggregationLoadMoreSkeleton). Here, between the list and the button, they would shorten the list. -->\n <!-- `variant=\"none\"`: galactik's ButtonComponent matches every <button> and would otherwise dress this one as a primary\n button (filled, rounded, 36px high). `none` leaves the classes below as the only style. -->\n <!-- `aria-disabled`, not `disabled`: the focus stays on the button while the page loads. The accessible name stays\n \"load more\" and `aria-busy` tells that it is working, so no extra text has to be translated by the application. -->\n <button\n type=\"button\"\n data-load-more=\"button\"\n variant=\"none\"\n class=\"mt-1 flex w-full shrink-0 cursor-pointer items-center justify-center rounded-(--radius-xs) px-2 py-1.5 text-(--font-primary-muted) underline underline-offset-4 hover:bg-(--bg-primary-lighter) hover:text-(--font-primary-base) focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-(--stroke-focus) focus-visible:outline-solid aria-busy:cursor-progress aria-busy:no-underline aria-busy:hover:bg-transparent\"\n [attr.aria-label]=\"'loadMore' | transloco\"\n [attr.aria-busy]=\"loading() ? 'true' : null\"\n [attr.aria-disabled]=\"loading() ? 'true' : null\"\n (click)=\"requestMore()\">\n <!-- Both states share one grid cell: the button keeps its width and height when the text gives way to the dots. -->\n <span class=\"inline-grid\">\n <span class=\"col-start-1 row-start-1\" [class.invisible]=\"loading()\">\n {{ \"loadMore\" | transloco }}\n </span>\n @if (loading()) {\n <loading-dots size=\"sm\" class=\"col-start-1 row-start-1\" />\n }\n </span>\n </button>\n }\n </div>\n</ng-template>\n\n@if (headless()) {\n <div class=\"flex min-h-0 flex-1 flex-col p-0.5\">\n <ng-container [ngTemplateOutlet]=\"bodySlot\" />\n </div>\n} @else {\n <details\n [attr.open]=\"expanded()\"\n [attr.name]=\"id()\"\n class=\"group flex min-h-0 flex-1 flex-col p-0.5 details-content:flex details-content:min-h-0 details-content:flex-col\"\n (toggle)=\"onToggle($event)\">\n <summary\n tabindex=\"-1\"\n [class.cursor-pointer]=\"collapsible() && !isEmpty()\"\n [class]=\"\n (isEmpty() ? 'text-(--font-neutral-muted) ' : '') + rowStateClass\n \"\n class=\"m-0 flex h-8 w-full shrink-0 scroll-mt-16 scroll-mb-3 items-center gap-1 pl-1 font-semibold select-none\"\n (click)=\"onHeaderClick($event)\">\n <ng-container [ngTemplateOutlet]=\"labelSlot\" />\n <ng-container [ngTemplateOutlet]=\"headerActionsSlot\" />\n </summary>\n <ng-container [ngTemplateOutlet]=\"bodySlot\" />\n </details>\n}\n",
|
|
26825
27438
|
styles: [":host{interpolate-size:allow-keywords}details::details-content{flex:1 1 auto;block-size:0;overflow:clip;overflow-clip-margin:.25rem;transition:block-size .2s cubic-bezier(.32,.72,0,1),content-visibility .2s allow-discrete}details[open]::details-content{block-size:auto}@media(prefers-reduced-motion:reduce){details::details-content{transition:none}}\n"],
|
|
26826
27439
|
dependencies: [
|
|
26827
27440
|
{
|
|
@@ -26966,7 +27579,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
26966
27579
|
],
|
|
26967
27580
|
providers: [provideTranslocoScope("filters")],
|
|
26968
27581
|
host: { class: "flex min-h-0 flex-1 flex-col" },
|
|
26969
|
-
template: "<!-- `<details>`/`<summary>` stay native (open/close, the `name`-based exclusive accordion): a facet is\n a disclosure, not a grid row. They used to sit in an `@angular/aria` grid so ArrowUp/ArrowDown could\n jump from one facet header to the next, but a grid without headers reads as a layout table\n (RGAA 5.3). The `<summary>` itself is `tabindex=\"-1\"`: Tab walks the header's chevron, never the header row, and the\n chevron is the keyboard way to collapse. Select all, clear and apply are not in the header: they are in the body, in a\n bar under the search field. Label, header actions (the count and the chevron) and body are `<ng-template>`s: `<ng-content select=\"…\">` can only be\n declared once, and the body is projected content. -->\n\n<ng-template #searchToggleSlot>\n <button\n #searchToggle\n type=\"button\"\n variant=\"tertiary\"\n [iconOnly]=\"true\"\n size=\"sm\"\n class=\"shrink-0 cursor-pointer\"\n [attr.aria-expanded]=\"showSearch()\"\n [attr.aria-controls]=\"searchId\"\n [tooltip]=\"\n 'filters.searchInFacet'\n | transloco: { facet: (aggregation()?.display | syslang | transloco) }\n \"\n (click)=\"toggleSearch($event)\">\n <search-icon class=\"shrink-0\" />\n <span class=\"sr-only\">{{\n \"filters.searchInFacet\"\n | transloco: { facet: (aggregation()?.display | syslang | transloco) }\n }}</span>\n </button>\n</ng-template>\n\n<ng-template #labelSlot>\n <ng-content select=\"label\">\n @let icon = aggregation()?.icon;\n @if (icon) {\n <FaIcon [faClass]=\"icon\" class=\"mr-1 shrink-0\" />\n }\n <span class=\"grow truncate\">{{\n aggregation()?.display | syslang | transloco\n }}</span>\n </ng-content>\n</ng-template>\n\n<ng-template #headerActionsSlot>\n @if (showFiltersCount() && filtersCount() > 0) {\n <Badge variant=\"number\" fill=\"primary\" scheme=\"sage\" size=\"xs\" class=\"ml-1\">\n {{ filtersCount() }}\n </Badge>\n }\n @if (canSearch() && !isCollapsed()) {\n <!-- The magnifier opens the search field of the facet (and closes it): the field is not on screen until then. It is\n in the header, next to the chevron, and only while the facet is open — there is nothing to search in a closed\n one. Interactive content in a summary does not toggle the details by itself, and the click is stopped anyway. -->\n <ng-container [ngTemplateOutlet]=\"searchToggleSlot\" />\n }\n @if (collapsible()) {\n <!-- The `<summary>` is out of the tab order (`tabindex=\"-1\"`), so this button is the keyboard way to\n collapse the facet. Its click bubbles to the `<summary>`, whose handler does the toggle, and it\n exposes `aria-expanded`. -->\n <button\n type=\"button\"\n variant=\"tertiary\"\n [iconOnly]=\"true\"\n size=\"sm\"\n [attr.aria-expanded]=\"!isCollapsed()\"\n [tooltip]=\"'filters.toggle' | transloco\"\n class=\"shrink-0 cursor-pointer [&_svg]:transition-transform [&_svg]:duration-150 group-open:[&_svg]:rotate-90\">\n <chevron-right class=\"shrink-0\" />\n <span class=\"sr-only\">{{ \"filters.toggle\" | transloco }}</span>\n </button>\n }\n</ng-template>\n\n<ng-template #bodySlot>\n <div class=\"flex min-h-0 flex-1 flex-col\">\n @if (canSearch()) {\n <!-- The field is always in the DOM and folded to nothing until it is asked for: a grid row that goes from 0fr to 1fr is\n the one height animation that needs no measuring. `inert` takes the folded field out of the tab order and of the\n accessibility tree. The margins live in the inner box: a padding on the folded element itself would keep it\n from reaching zero.\n\n No `hotkey`: this field is one of many facet search boxes on the page, so it must not advertise a global shortcut.\n A placeholder is not a label (RGAA 11.1): the name says WHICH facet this box filters — the one thing that tells a\n dozen identical fields apart. SearchInput hands its class to its inner field, so a margin on it would count twice,\n which is one more reason for the padding on the box around it. -->\n <div\n [id]=\"searchId\"\n class=\"grid shrink-0 transition-[grid-template-rows,opacity] duration-200 ease-[cubic-bezier(0.32,0.72,0,1)] motion-reduce:transition-none\"\n [class]=\"\n showSearch()\n ? 'grid-rows-[1fr] opacity-100'\n : 'grid-rows-[0fr] opacity-0'\n \"\n [inert]=\"!showSearch()\">\n <!-- `-mx-1 px-1`: the box clips what is outside it, and the focus ring of the field is drawn outside the field,\n so it needs a few pixels of room on each side — taken back by the negative margin so nothing moves. -->\n <div class=\"-mx-1 min-h-0 overflow-hidden px-1\">\n <div [class]=\"headless() ? 'pt-0.5 pb-1' : 'pt-2 pb-1'\">\n <SearchInput\n [placeholder]=\"'search' | transloco\"\n [clearLabel]=\"'filters.clearSearch' | transloco\"\n [aria-label]=\"\n 'filters.searchInFacet'\n | transloco\n : { facet: (aggregation()?.display | syslang | transloco) }\n \"\n [(value)]=\"searchText\"\n (keydown.escape)=\"onSearchEscape($event)\">\n <ng-content select=\"[search-addon]\" />\n </SearchInput>\n </div>\n </div>\n </div>\n }\n\n @if (itemsLength() > 0 || hasFilters() || selection()) {\n <!-- The bar under the search field, in a popover and in the filters drawer alike: clear and apply, and nothing else, so\n both keep their full text in the little room there is. Clear is a button with a text, not an icon, and stays in\n place (disabled) when there is nothing to clear. Apply only shows once something is pending, as it always did, and\n sits at the same level. -->\n <div\n data-bar=\"actions\"\n class=\"mb-1 flex shrink-0 items-center justify-end gap-1\">\n @if (headless() && canSearch()) {\n <ng-container [ngTemplateOutlet]=\"searchToggleSlot\" />\n }\n <button\n type=\"button\"\n variant=\"tertiary\"\n size=\"sm\"\n class=\"shrink-0\"\n [disabled]=\"!hasFilters() && selectedCount() === 0\"\n [attr.aria-label]=\"'filters.clearFilters' | transloco\"\n (click)=\"cleared.emit()\">\n {{ \"filters.clearAll\" | transloco }}\n </button>\n @if (selection()) {\n <!-- No icon: the room is short, and the word says it. -->\n <button\n type=\"button\"\n variant=\"accent\"\n size=\"sm\"\n class=\"shrink-0 overflow-hidden\"\n (click)=\"applied.emit()\">\n <span class=\"truncate\">{{ \"filters.apply\" | transloco }}</span>\n </button>\n }\n </div>\n }\n\n @if (itemsLength() > 0) {\n <!-- Select all is the first row of the list, written like one: the box lines up with the boxes of the items it drives and\n carries its own text, which a bare box next to two buttons never did. The text lines up with the items' too: same gap\n after the box (`gap-2`) and same size (`text-sm`, where the checkbox's own label would be 12px). The whole row is the label, so it is as easy\n to hit as an item. The count of checked items is part of that same label (\"Select all (3)\"), so the two can never\n overlap: when the room is short the label is cut with an ellipsis as a whole. It stays out of the scroller of the list, so it does not scroll away with the items. -->\n <div\n data-bar=\"select-all\"\n class=\"mb-1 flex shrink-0 items-center pr-3 pl-1.5 text-sm\">\n <checkbox\n class=\"min-w-0 flex-1 items-center gap-2 py-1 [&_.checkbox-label]:truncate [&_.checkbox-label]:text-sm [&_.checkbox-label]:leading-6 [&_.checkbox-text-block]:min-w-0\"\n [checked]=\"isAllSelected()\"\n [indeterminate]=\"selectedCount() > 0 && !isAllSelected()\"\n (checkedChange)=\"$event ? allSelected.emit() : allUnselected.emit()\">\n {{\n (isAllSelected() ? \"filters.unselectAll\" : \"filters.selectAll\")\n | transloco\n }}\n @if (selectedCount() > 0) {\n <span class=\"text-(--font-primary-muted)\"\n >({{ selectedCount() }})</span\n >\n }\n </checkbox>\n <span class=\"sr-only\" aria-live=\"polite\">{{\n \"filters.selectedCount\" | transloco: { count: selectedCount() }\n }}</span>\n </div>\n }\n\n <ng-content />\n\n @if (hasMore() && searchedItemsLength() === 0) {\n <!-- The placeholder rows of a loading page are not here: they are rows of the scrolling area, in the list and the tree\n (AggregationLoadMoreSkeleton). Here, between the list and the button, they would shorten the list. -->\n <!-- `variant=\"none\"`: galactik's ButtonComponent matches every <button> and would otherwise dress this one as a primary\n button (filled, rounded, 36px high). `none` leaves the classes below as the only style. -->\n <!-- `aria-disabled`, not `disabled`: the focus stays on the button while the page loads. The accessible name stays\n \"load more\" and `aria-busy` tells that it is working, so no extra text has to be translated by the application. -->\n <button\n type=\"button\"\n data-load-more=\"button\"\n variant=\"none\"\n class=\"mt-1 flex w-full shrink-0 cursor-pointer items-center justify-center rounded-(--radius-xs) px-2 py-1.5 text-(--font-primary-muted) underline underline-offset-4 hover:bg-(--bg-primary-lighter) hover:text-(--font-primary-base) focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-(--stroke-focus) focus-visible:outline-solid aria-busy:cursor-progress aria-busy:no-underline aria-busy:hover:bg-transparent\"\n [attr.aria-label]=\"'loadMore' | transloco\"\n [attr.aria-busy]=\"loading() ? 'true' : null\"\n [attr.aria-disabled]=\"loading() ? 'true' : null\"\n (click)=\"requestMore()\">\n <!-- Both states share one grid cell: the button keeps its width and height when the text gives way to the dots. -->\n <span class=\"inline-grid\">\n <span class=\"col-start-1 row-start-1\" [class.invisible]=\"loading()\">\n {{ \"loadMore\" | transloco }}\n </span>\n @if (loading()) {\n <loading-dots size=\"sm\" class=\"col-start-1 row-start-1\" />\n }\n </span>\n </button>\n }\n </div>\n</ng-template>\n\n@if (headless()) {\n <div class=\"flex min-h-0 flex-1 flex-col p-0.5\">\n <ng-container [ngTemplateOutlet]=\"bodySlot\" />\n </div>\n} @else {\n <details\n [attr.open]=\"expanded()\"\n [attr.name]=\"id()\"\n class=\"group flex min-h-0 flex-1 flex-col p-0.5 details-content:flex details-content:min-h-0 details-content:flex-col\"\n (toggle)=\"onToggle($event)\">\n <summary\n tabindex=\"-1\"\n [class.cursor-pointer]=\"collapsible() && !isEmpty()\"\n [class]=\"\n (isEmpty() ? 'text-(--font-neutral-muted) ' : '') + rowStateClass\n \"\n class=\"m-0 flex h-8 w-full shrink-0 scroll-mt-16 scroll-mb-3 items-center gap-1 pl-1 font-semibold select-none\"\n (click)=\"onHeaderClick($event)\">\n <ng-container [ngTemplateOutlet]=\"labelSlot\" />\n <ng-container [ngTemplateOutlet]=\"headerActionsSlot\" />\n </summary>\n <ng-container [ngTemplateOutlet]=\"bodySlot\" />\n </details>\n}\n",
|
|
27582
|
+
template: "<!-- `<details>`/`<summary>` stay native (open/close, the `name`-based exclusive accordion): a facet is\n a disclosure, not a grid row. They used to sit in an `@angular/aria` grid so ArrowUp/ArrowDown could\n jump from one facet header to the next, but a grid without headers reads as a layout table\n (RGAA 5.3). The `<summary>` itself is `tabindex=\"-1\"`: Tab walks the header's chevron, never the header row, and the\n chevron is the keyboard way to collapse. Select all, clear and apply are not in the header: they are in the body, in a\n bar under the search field. Label, header actions (the count and the chevron) and body are `<ng-template>`s: `<ng-content select=\"…\">` can only be\n declared once, and the body is projected content. -->\n\n<ng-template #searchToggleSlot>\n <button\n #searchToggle\n type=\"button\"\n variant=\"tertiary\"\n [iconOnly]=\"true\"\n size=\"md\"\n class=\"shrink-0 cursor-pointer\"\n [attr.aria-expanded]=\"showSearch()\"\n [attr.aria-controls]=\"searchId\"\n [tooltip]=\"\n 'filters.searchInFacet'\n | transloco: { facet: (aggregation()?.display | syslang | transloco) }\n \"\n (click)=\"toggleSearch($event)\">\n <search-icon class=\"shrink-0\" />\n <span class=\"sr-only\">{{\n \"filters.searchInFacet\"\n | transloco: { facet: (aggregation()?.display | syslang | transloco) }\n }}</span>\n </button>\n</ng-template>\n\n<ng-template #labelSlot>\n <ng-content select=\"label\">\n @let icon = aggregation()?.icon;\n @if (icon) {\n <FaIcon [faClass]=\"icon\" class=\"mr-1 shrink-0\" />\n }\n <span class=\"grow truncate\">{{\n aggregation()?.display | syslang | transloco\n }}</span>\n </ng-content>\n</ng-template>\n\n<ng-template #headerActionsSlot>\n @if (showFiltersCount() && filtersCount() > 0) {\n <Badge variant=\"number\" fill=\"primary\" scheme=\"sage\" size=\"xs\" class=\"ml-1\">\n {{ filtersCount() }}\n </Badge>\n }\n @if (canSearch() && !isCollapsed()) {\n <!-- The magnifier opens the search field of the facet (and closes it): the field is not on screen until then. It is\n in the header, next to the chevron, and only while the facet is open — there is nothing to search in a closed\n one. Interactive content in a summary does not toggle the details by itself, and the click is stopped anyway. -->\n <ng-container [ngTemplateOutlet]=\"searchToggleSlot\" />\n }\n @if (collapsible()) {\n <!-- The `<summary>` is out of the tab order (`tabindex=\"-1\"`), so this button is the keyboard way to\n collapse the facet. Its click bubbles to the `<summary>`, whose handler does the toggle, and it\n exposes `aria-expanded`. -->\n <button\n type=\"button\"\n variant=\"tertiary\"\n [iconOnly]=\"true\"\n size=\"md\"\n [attr.aria-expanded]=\"!isCollapsed()\"\n [tooltip]=\"'filters.toggle' | transloco\"\n class=\"shrink-0 cursor-pointer [&_svg]:transition-transform [&_svg]:duration-150 group-open:[&_svg]:rotate-90\">\n <chevron-right class=\"shrink-0\" />\n <span class=\"sr-only\">{{ \"filters.toggle\" | transloco }}</span>\n </button>\n }\n</ng-template>\n\n<ng-template #bodySlot>\n <div class=\"flex min-h-0 flex-1 flex-col\">\n @if (canSearch()) {\n <!-- The field is always in the DOM and folded to nothing until it is asked for: a grid row that goes from 0fr to 1fr is\n the one height animation that needs no measuring. `inert` takes the folded field out of the tab order and of the\n accessibility tree. The margins live in the inner box: a padding on the folded element itself would keep it\n from reaching zero.\n\n No `hotkey`: this field is one of many facet search boxes on the page, so it must not advertise a global shortcut.\n A placeholder is not a label (RGAA 11.1): the name says WHICH facet this box filters — the one thing that tells a\n dozen identical fields apart. SearchInput hands its class to its inner field, so a margin on it would count twice,\n which is one more reason for the padding on the box around it. -->\n <div\n [id]=\"searchId\"\n class=\"grid shrink-0 transition-[grid-template-rows,opacity] duration-200 ease-[cubic-bezier(0.32,0.72,0,1)] motion-reduce:transition-none\"\n [class]=\"\n showSearch()\n ? 'grid-rows-[1fr] opacity-100'\n : 'grid-rows-[0fr] opacity-0'\n \"\n [inert]=\"!showSearch()\">\n <!-- `px-0.5`: the box clips what is outside it, and the focus ring of the field is drawn outside the field (1px past its\n border), so it needs a little room on each side. No negative margin to take it back: it pushed that room OUTSIDE\n the box, into the clipping of the popover or the drawer around, and the ring was cut on both sides. -->\n <div class=\"min-h-0 overflow-hidden px-0.5\">\n <div [class]=\"headless() ? 'pt-0.5 pb-1' : 'pt-2 pb-1'\">\n <SearchInput\n [placeholder]=\"'search' | transloco\"\n [clearLabel]=\"'filters.clearSearch' | transloco\"\n [aria-label]=\"\n 'filters.searchInFacet'\n | transloco\n : { facet: (aggregation()?.display | syslang | transloco) }\n \"\n [(value)]=\"searchText\"\n (keydown.escape)=\"onSearchEscape($event)\">\n <ng-content select=\"[search-addon]\" />\n </SearchInput>\n </div>\n </div>\n </div>\n }\n\n @if (itemsLength() > 0 || hasFilters() || selection()) {\n <!-- The bar under the search field, in a popover and in the filters drawer alike: clear and apply, and nothing else, so\n both keep their full text in the little room there is. Clear is a button with a text, not an icon, and stays in\n place (disabled) when there is nothing to clear. Apply only shows once something is pending, as it always did, and\n sits at the same level. -->\n <div\n data-bar=\"actions\"\n class=\"mb-1 flex shrink-0 items-center justify-end gap-1\">\n @if (headless() && canSearch()) {\n <ng-container [ngTemplateOutlet]=\"searchToggleSlot\" />\n }\n <button\n type=\"button\"\n variant=\"tertiary\"\n size=\"lg\"\n class=\"shrink-0\"\n [disabled]=\"!hasFilters() && selectedCount() === 0\"\n [attr.aria-label]=\"'filters.clearFilters' | transloco\"\n (click)=\"cleared.emit()\">\n {{ \"filters.clearAll\" | transloco }}\n </button>\n @if (selection()) {\n <!-- No icon: the room is short, and the word says it. -->\n <button\n type=\"button\"\n variant=\"accent\"\n size=\"lg\"\n class=\"shrink-0 overflow-hidden\"\n (click)=\"applied.emit()\">\n <span class=\"truncate\">{{ \"filters.apply\" | transloco }}</span>\n </button>\n }\n </div>\n }\n\n @if (itemsLength() > 0) {\n <!-- Select all is the first row of the list, written like one: the box lines up with the boxes of the items it drives and\n carries its own text, which a bare box next to two buttons never did. The text lines up with the items' too: same gap\n after the box (`gap-2`) and same size (`text-sm`, where the checkbox's own label would be 12px). The whole row is the label, so it is as easy\n to hit as an item. The count of checked items is part of that same label (\"Select all (3)\"), so the two can never\n overlap: when the room is short the label is cut with an ellipsis as a whole. It stays out of the scroller of the list, so it does not scroll away with the items. -->\n <div\n data-bar=\"select-all\"\n class=\"mb-1 flex shrink-0 items-center pr-3 pl-1.5 text-sm\">\n <checkbox\n class=\"min-w-0 flex-1 items-center gap-2 py-1.5 [&_.checkbox-label]:truncate [&_.checkbox-label]:text-base [&_.checkbox-label]:leading-6 [&_.checkbox-text-block]:min-w-0\"\n [checked]=\"isAllSelected()\"\n [indeterminate]=\"selectedCount() > 0 && !isAllSelected()\"\n (checkedChange)=\"$event ? allSelected.emit() : allUnselected.emit()\">\n {{\n (isAllSelected() ? \"filters.unselectAll\" : \"filters.selectAll\")\n | transloco\n }}\n @if (selectedCount() > 0) {\n <span class=\"text-(--font-primary-muted)\"\n >({{ selectedCount() }})</span\n >\n }\n </checkbox>\n <span class=\"sr-only\" aria-live=\"polite\">{{\n \"filters.selectedCount\" | transloco: { count: selectedCount() }\n }}</span>\n </div>\n }\n\n <ng-content />\n\n @if (hasMore() && searchedItemsLength() === 0) {\n <!-- The placeholder rows of a loading page are not here: they are rows of the scrolling area, in the list and the tree\n (AggregationLoadMoreSkeleton). Here, between the list and the button, they would shorten the list. -->\n <!-- `variant=\"none\"`: galactik's ButtonComponent matches every <button> and would otherwise dress this one as a primary\n button (filled, rounded, 36px high). `none` leaves the classes below as the only style. -->\n <!-- `aria-disabled`, not `disabled`: the focus stays on the button while the page loads. The accessible name stays\n \"load more\" and `aria-busy` tells that it is working, so no extra text has to be translated by the application. -->\n <button\n type=\"button\"\n data-load-more=\"button\"\n variant=\"none\"\n class=\"mt-1 flex w-full shrink-0 cursor-pointer items-center justify-center rounded-(--radius-xs) px-2 py-1.5 text-(--font-primary-muted) underline underline-offset-4 hover:bg-(--bg-primary-lighter) hover:text-(--font-primary-base) focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-(--stroke-focus) focus-visible:outline-solid aria-busy:cursor-progress aria-busy:no-underline aria-busy:hover:bg-transparent\"\n [attr.aria-label]=\"'loadMore' | transloco\"\n [attr.aria-busy]=\"loading() ? 'true' : null\"\n [attr.aria-disabled]=\"loading() ? 'true' : null\"\n (click)=\"requestMore()\">\n <!-- Both states share one grid cell: the button keeps its width and height when the text gives way to the dots. -->\n <span class=\"inline-grid\">\n <span class=\"col-start-1 row-start-1\" [class.invisible]=\"loading()\">\n {{ \"loadMore\" | transloco }}\n </span>\n @if (loading()) {\n <loading-dots size=\"sm\" class=\"col-start-1 row-start-1\" />\n }\n </span>\n </button>\n }\n </div>\n</ng-template>\n\n@if (headless()) {\n <div class=\"flex min-h-0 flex-1 flex-col p-0.5\">\n <ng-container [ngTemplateOutlet]=\"bodySlot\" />\n </div>\n} @else {\n <details\n [attr.open]=\"expanded()\"\n [attr.name]=\"id()\"\n class=\"group flex min-h-0 flex-1 flex-col p-0.5 details-content:flex details-content:min-h-0 details-content:flex-col\"\n (toggle)=\"onToggle($event)\">\n <summary\n tabindex=\"-1\"\n [class.cursor-pointer]=\"collapsible() && !isEmpty()\"\n [class]=\"\n (isEmpty() ? 'text-(--font-neutral-muted) ' : '') + rowStateClass\n \"\n class=\"m-0 flex h-8 w-full shrink-0 scroll-mt-16 scroll-mb-3 items-center gap-1 pl-1 font-semibold select-none\"\n (click)=\"onHeaderClick($event)\">\n <ng-container [ngTemplateOutlet]=\"labelSlot\" />\n <ng-container [ngTemplateOutlet]=\"headerActionsSlot\" />\n </summary>\n <ng-container [ngTemplateOutlet]=\"bodySlot\" />\n </details>\n}\n",
|
|
26970
27583
|
styles: [":host{interpolate-size:allow-keywords}details::details-content{flex:1 1 auto;block-size:0;overflow:clip;overflow-clip-margin:.25rem;transition:block-size .2s cubic-bezier(.32,.72,0,1),content-visibility .2s allow-discrete}details[open]::details-content{block-size:auto}@media(prefers-reduced-motion:reduce){details::details-content{transition:none}}\n"]
|
|
26971
27584
|
}]
|
|
26972
27585
|
}],
|
|
@@ -27849,7 +28462,7 @@ var AggregationTreeComponent = class AggregationTreeComponent {
|
|
|
27849
28462
|
isSignal: true
|
|
27850
28463
|
}],
|
|
27851
28464
|
ngImport: i0,
|
|
27852
|
-
template: "@if (!aggregation()?.isTree) {\n <div role=\"alert\" class=\"p-2 text-sm text-red-500\">\n <triangle-alert-icon class=\"mr-1 shrink-0\" />\n The aggregationTree component does not support list aggregations. Please use\n the <Aggregation /> component instead.\n </div>\n}\n<aggregation-panel\n [id]=\"id()\"\n [collapsible]=\"collapsible()\"\n [collapsed]=\"collapsed()\"\n [isEmpty]=\"isEmpty()\"\n [isDate]=\"isDate()\"\n [aggregation]=\"aggregation()\"\n [showFiltersCount]=\"showFiltersCount()\"\n [filtersCount]=\"filtersCount()\"\n [hasFilters]=\"hasFilters()\"\n [selection]=\"selection()\"\n [isAllSelected]=\"isAllSelected()\"\n [headless]=\"headless()\"\n [selectedCount]=\"selected().length\"\n [(searchText)]=\"searchText\"\n [itemsLength]=\"items().length\"\n [hasMore]=\"aggregation()?.$hasMore ?? false\"\n [loading]=\"loadingMore()\"\n [searchedItemsLength]=\"searchedItems().length\"\n (cleared)=\"clear()\"\n (applied)=\"apply()\"\n (allSelected)=\"selectAll()\"\n (allUnselected)=\"unselectAll()\"\n (loadedMore)=\"loadMore()\">\n <!-- Relay the host's `label` projection into the panel's `label` slot. When the\n host projects nothing, the local <ng-content> falls back to the icon + display\n (mirrors AggregationPanel's own fallback), so the header is never empty. -->\n <ng-container ngProjectAs=\"label\">\n <ng-content select=\"label\">\n @let labelIcon = aggregation()?.icon;\n @if (labelIcon) {\n <FaIcon [faClass]=\"labelIcon\" class=\"mr-1 shrink-0\" />\n }\n <span class=\"grow truncate\">{{\n aggregation()?.display | syslang | transloco\n }}</span>\n </ng-content>\n </ng-container>\n <div class=\"min-h-0 w-full flex-1 scrollbar-thin overflow-auto pb-3\">\n <ul\n ngTree\n #tree=\"ngTree\"\n [multi]=\"true\"\n [(value)]=\"selected\"\n (valueChange)=\"onSelectionChange($event)\"\n /* Translated like the flat list's own label (aggregation-list.html): this was the raw\n `display`, so the accessible name of a whole facet came out in the source language.\n The pipes apply to the whole nullish chain, `|` being the loosest operator Angular\n parses — no parentheses needed. */\n [attr.aria-label]=\"aggregation()?.display ?? name() | syslang | transloco\"\n class=\"basic-tree m-1\">\n <ng-template\n [ngTemplateOutlet]=\"treeNodes\"\n [ngTemplateOutletContext]=\"{ nodes: nodes(), parent: tree }\" />\n </ul>\n @if (loadingMore() && searchedItems().length === 0) {\n <AggregationLoadMoreSkeleton variant=\"tree\" [showCount]=\"showCount()\" />\n }\n </div>\n</aggregation-panel>\n\n<ng-template #treeNodes let-nodes=\"nodes\" let-parent=\"parent\">\n @for (node of nodes; track node.$path) {\n @let label = node.display ?? node.value ?? \"\";\n <li\n ngTreeItem\n #treeItem=\"ngTreeItem\"\n [parent]=\"parent\"\n [value]=\"node.$path\"\n [label]=\"label\"\n [disabled]=\"node.disabled\"\n [(expanded)]=\"node.$opened\"\n (expandedChange)=\"onExpandedChange(node, $event)\"\n [attr.aria-label]=\"label\"\n [attr.aria-describedby]=\"\n nodeDescription(node) ? descriptionId(node.$path) : null\n \"\n [class]=\"cn('m-0 block w-full p-0', itemStateClass)\"\n (keydown.enter)=\"onTreeItemEnterKey(node, $event)\"\n (keydown.space)=\"onTreeItemSpaceKey(node, $event)\">\n <!-- 2. Ce div prend tout l'espace et intercepte tous les clics de la ligne. `ps-0.5`: the checkbox of a list option sits\n 2px further in than a tree's (the option has its own padding), so the checkboxes of the two facets, and the select-all of\n the bar of a popover, are on the same vertical line. -->\n <div\n class=\"flex w-full cursor-pointer items-center gap-2 py-0.5 ps-0.5 text-sm leading-6\"\n (click)=\"onTreeItemRowClick(node, $event)\"\n (mousedown)=\"$event.stopPropagation()\"\n (pointerdown)=\"$event.stopPropagation()\">\n @if (showCheckbox()) {\n <!-- `inert` keeps the native input out of the tab order and out of hit-testing:\n a role=\"treeitem\" admits no focusable descendant, and the click must reach\n the row handler above. Selection is announced by the <li>'s aria-selected. -->\n <!-- --spacing-xs: 0 kills the gap the checkbox keeps between its box and its\n (here empty) text block. Set as a custom property rather than a utility\n class: the CVA already sets gap-(--spacing-xs), and two same-specificity\n classes would be resolved by stylesheet order, not by us. -->\n <span\n inert\n aria-hidden=\"true\"\n class=\"inline-flex shrink-0\"\n style=\"--spacing-xs: 0\">\n <checkbox\n [checked]=\"treeItem.selected() || node.$selectedVisually || false\"\n [disabled]=\"node.disabled\"\n [class]=\"\n cn(\n 'pointer-events-none',\n (treeItem.selected() || node.$selectedVisually) &&\n 'selected-icon'\n )\n \" />\n </span>\n }\n <!-- Depth indent sits on the chevron, after the checkbox: the checkboxes of every\n level stay flush left (aligned with a flat list's), and since this adds no extra\n flex child, the step between consecutive levels is exactly one indent. -->\n @let depthIndent =\n \"calc(var(--agg-tree-indent, 0.5rem) * \" +\n (treeItem.level() - 1) +\n \")\";\n @if (node.hasChildren) {\n <!-- Expand/collapse affordance for the mouse only, hence a <span> and not a <button>:\n the keyboard already expands with ArrowRight / ArrowLeft, handled by ngTreeItem, and\n a role=\"treeitem\" admits no focusable descendant — so this was a <button> nobody\n could reach, already carrying aria-hidden and tabindex=\"-1\" to say so. Every audit\n reads it as a button with no accessible name (RGAA 7.1, 11.9). Same shape as the leaf\n branch below. -->\n <span\n aria-hidden=\"true\"\n (click)=\"toggleTreeNode(treeItem, $event)\"\n [style.marginInlineStart]=\"depthIndent\"\n class=\"inline-flex cursor-pointer items-center justify-center\">\n <ChevronRightIcon\n [class]=\"\n cn('expand-icon shrink-0', node.$opened && 'rotate-90')\n \" />\n <FolderIcon class=\"shrink-0\" />\n </span>\n } @else {\n <span\n aria-hidden=\"true\"\n [style.marginInlineStart]=\"depthIndent\"\n class=\"inline-flex items-center justify-center\">\n <!-- Reserves the chevron's own width (`.expand-icon` is a styling hook with\n no rule of its own), so a leaf lines up with its parent nodes. -->\n <span class=\"expand-icon w-4 shrink-0\"></span>\n <FileIcon class=\"size-4 shrink-0\" />\n </span>\n }\n @if (resolvedItemTemplate(); as tpl) {\n <!-- Cut off, the label shows whole in a tooltip; a label that fits needs none. -->\n <span\n class=\"grow truncate select-none\"\n truncated\n #cutCustom=\"truncated\"\n [tooltip]=\"label\"\n [tooltipDisabled]=\"!cutCustom.isTruncated()\"\n (click)=\"onTreeItemTextClick(node, $event)\">\n <ng-container\n [ngTemplateOutlet]=\"tpl\"\n [ngTemplateOutletContext]=\"{\n $implicit: node,\n field: aggregation()?.column,\n selected: treeItem.selected() ?? false,\n count: node.count,\n name: label,\n searchText: searchText(),\n level: node.$level ?? 0,\n expanded: node.$opened ?? false,\n hasChildren: node.hasChildren ?? false\n }\" />\n </span>\n } @else {\n <!-- ce span est essentiel pour le style -->\n @let appliesOnLabelClick = quickFilter() || applyOnSelect();\n <!-- Cut off, the label shows whole in a tooltip (`truncated` measures it); a label that fits has none. -->\n <span\n [class]=\"\n cn(\n 'grow truncate select-none',\n appliesOnLabelClick && 'hover:underline'\n )\n \"\n truncated\n #cut=\"truncated\"\n [tooltip]=\"label\"\n [tooltipDisabled]=\"!cut.isTruncated()\"\n (click)=\"onTreeItemTextClick(node, $event)\">\n @for (\n chunk of label | highlightWord: searchText() : 10;\n track $index\n ) {\n <span [class]=\"{ 'font-bold': chunk.match }\" aria-hidden=\"true\">{{\n chunk.text\n }}</span>\n }\n </span>\n @if (showCount() && node.count > 0) {\n <span class=\"ml-auto px-1 text-xs\" aria-hidden=\"true\">{{\n node.count\n }}</span>\n }\n }\n @let description = nodeDescription(node);\n @if (description) {\n <!-- `hidden`, not `sr-only`: read through aria-describedby only. A visually-hidden\n span would also be counted in the treeitem's name (computed from its content). -->\n <span hidden [id]=\"descriptionId(node.$path)\">{{ description }}</span>\n }\n </div>\n </li>\n @if (node.hasChildren) {\n <!-- No indentation here: it would shift the checkbox too. Depth is applied\n inside the row, after the checkbox, so every checkbox stays flush left. -->\n <!-- The <ul> sits INSIDE the deferred content, not around it: `ngTreeItemGroup` only\n instantiates its view when the node opens, so a <ul> declared outside stayed in the DOM,\n empty, for every collapsed node — a structural tag with nothing to structure (RGAA 8.9).\n The @if cannot move onto the <ul> either: the directive registers itself with its tree\n item on init and has to exist for as long as the item does. -->\n <ng-template\n ngTreeItemGroup\n [ownedBy]=\"treeItem\"\n #group=\"ngTreeItemGroup\">\n <ul role=\"group\">\n <ng-template\n [ngTemplateOutlet]=\"treeNodes\"\n [ngTemplateOutletContext]=\"{ nodes: node.items, parent: group }\" />\n </ul>\n </ng-template>\n }\n }\n</ng-template>\n",
|
|
28465
|
+
template: "@if (!aggregation()?.isTree) {\n <div role=\"alert\" class=\"p-2 text-sm text-red-500\">\n <triangle-alert-icon class=\"mr-1 shrink-0\" />\n The aggregationTree component does not support list aggregations. Please use\n the <Aggregation /> component instead.\n </div>\n}\n<aggregation-panel\n [id]=\"id()\"\n [collapsible]=\"collapsible()\"\n [collapsed]=\"collapsed()\"\n [isEmpty]=\"isEmpty()\"\n [isDate]=\"isDate()\"\n [aggregation]=\"aggregation()\"\n [showFiltersCount]=\"showFiltersCount()\"\n [filtersCount]=\"filtersCount()\"\n [hasFilters]=\"hasFilters()\"\n [selection]=\"selection()\"\n [isAllSelected]=\"isAllSelected()\"\n [headless]=\"headless()\"\n [selectedCount]=\"selected().length\"\n [(searchText)]=\"searchText\"\n [itemsLength]=\"items().length\"\n [hasMore]=\"aggregation()?.$hasMore ?? false\"\n [loading]=\"loadingMore()\"\n [searchedItemsLength]=\"searchedItems().length\"\n (cleared)=\"clear()\"\n (applied)=\"apply()\"\n (allSelected)=\"selectAll()\"\n (allUnselected)=\"unselectAll()\"\n (loadedMore)=\"loadMore()\">\n <!-- Relay the host's `label` projection into the panel's `label` slot. When the\n host projects nothing, the local <ng-content> falls back to the icon + display\n (mirrors AggregationPanel's own fallback), so the header is never empty. -->\n <ng-container ngProjectAs=\"label\">\n <ng-content select=\"label\">\n @let labelIcon = aggregation()?.icon;\n @if (labelIcon) {\n <FaIcon [faClass]=\"labelIcon\" class=\"mr-1 shrink-0\" />\n }\n <span class=\"grow truncate\">{{\n aggregation()?.display | syslang | transloco\n }}</span>\n </ng-content>\n </ng-container>\n <div class=\"min-h-0 w-full flex-1 scrollbar-thin overflow-auto pb-3\">\n <ul\n ngTree\n #tree=\"ngTree\"\n [multi]=\"true\"\n [(value)]=\"selected\"\n (valueChange)=\"onSelectionChange($event)\"\n /* Translated like the flat list's own label (aggregation-list.html): this was the raw\n `display`, so the accessible name of a whole facet came out in the source language.\n The pipes apply to the whole nullish chain, `|` being the loosest operator Angular\n parses — no parentheses needed. */\n [attr.aria-label]=\"aggregation()?.display ?? name() | syslang | transloco\"\n class=\"basic-tree m-1\">\n <ng-template\n [ngTemplateOutlet]=\"treeNodes\"\n [ngTemplateOutletContext]=\"{ nodes: nodes(), parent: tree }\" />\n </ul>\n @if (loadingMore() && searchedItems().length === 0) {\n <AggregationLoadMoreSkeleton variant=\"tree\" [showCount]=\"showCount()\" />\n }\n </div>\n</aggregation-panel>\n\n<ng-template #treeNodes let-nodes=\"nodes\" let-parent=\"parent\">\n @for (node of nodes; track node.$path) {\n @let label = node.display ?? node.value ?? \"\";\n <li\n ngTreeItem\n #treeItem=\"ngTreeItem\"\n [parent]=\"parent\"\n [value]=\"node.$path\"\n [label]=\"label\"\n [disabled]=\"node.disabled\"\n [(expanded)]=\"node.$opened\"\n (expandedChange)=\"onExpandedChange(node, $event)\"\n [attr.aria-label]=\"label\"\n [attr.aria-describedby]=\"\n nodeDescription(node) ? descriptionId(node.$path) : null\n \"\n [class]=\"cn('m-0 block w-full p-0', itemStateClass)\"\n (keydown.enter)=\"onTreeItemEnterKey(node, $event)\"\n (keydown.space)=\"onTreeItemSpaceKey(node, $event)\">\n <!-- 2. Ce div prend tout l'espace et intercepte tous les clics de la ligne. `ps-0.5`: the checkbox of a list option sits\n 2px further in than a tree's (the option has its own padding), so the checkboxes of the two facets, and the select-all of\n the bar of a popover, are on the same vertical line. -->\n <div\n class=\"flex w-full cursor-pointer items-center gap-2 py-1.5 ps-0.5 text-base leading-6\"\n (click)=\"onTreeItemRowClick(node, $event)\"\n (mousedown)=\"$event.stopPropagation()\"\n (pointerdown)=\"$event.stopPropagation()\">\n @if (showCheckbox()) {\n <!-- `inert` keeps the native input out of the tab order and out of hit-testing:\n a role=\"treeitem\" admits no focusable descendant, and the click must reach\n the row handler above. Selection is announced by the <li>'s aria-selected. -->\n <!-- --spacing-xs: 0 kills the gap the checkbox keeps between its box and its\n (here empty) text block. Set as a custom property rather than a utility\n class: the CVA already sets gap-(--spacing-xs), and two same-specificity\n classes would be resolved by stylesheet order, not by us. -->\n <span\n inert\n aria-hidden=\"true\"\n class=\"inline-flex shrink-0\"\n style=\"--spacing-xs: 0\">\n <checkbox\n [checked]=\"treeItem.selected() || node.$selectedVisually || false\"\n [disabled]=\"node.disabled\"\n [class]=\"\n cn(\n 'pointer-events-none',\n (treeItem.selected() || node.$selectedVisually) &&\n 'selected-icon'\n )\n \" />\n </span>\n }\n <!-- Depth indent sits on the chevron, after the checkbox: the checkboxes of every\n level stay flush left (aligned with a flat list's), and since this adds no extra\n flex child, the step between consecutive levels is exactly one indent. -->\n @let depthIndent =\n \"calc(var(--agg-tree-indent, 0.5rem) * \" +\n (treeItem.level() - 1) +\n \")\";\n @if (node.hasChildren) {\n <!-- Expand/collapse affordance for the mouse only, hence a <span> and not a <button>:\n the keyboard already expands with ArrowRight / ArrowLeft, handled by ngTreeItem, and\n a role=\"treeitem\" admits no focusable descendant — so this was a <button> nobody\n could reach, already carrying aria-hidden and tabindex=\"-1\" to say so. Every audit\n reads it as a button with no accessible name (RGAA 7.1, 11.9). Same shape as the leaf\n branch below. -->\n <span\n aria-hidden=\"true\"\n (click)=\"toggleTreeNode(treeItem, $event)\"\n [style.marginInlineStart]=\"depthIndent\"\n class=\"inline-flex cursor-pointer items-center justify-center\">\n <ChevronRightIcon\n [class]=\"\n cn('expand-icon shrink-0', node.$opened && 'rotate-90')\n \" />\n <FolderIcon class=\"shrink-0\" />\n </span>\n } @else {\n <span\n aria-hidden=\"true\"\n [style.marginInlineStart]=\"depthIndent\"\n class=\"inline-flex items-center justify-center\">\n <!-- Reserves the chevron's own width (`.expand-icon` is a styling hook with\n no rule of its own), so a leaf lines up with its parent nodes. -->\n <span class=\"expand-icon w-4 shrink-0\"></span>\n <FileIcon class=\"size-4 shrink-0\" />\n </span>\n }\n @if (resolvedItemTemplate(); as tpl) {\n <!-- Cut off, the label shows whole in a tooltip; a label that fits needs none. -->\n <span\n class=\"grow truncate select-none\"\n truncated\n #cutCustom=\"truncated\"\n [tooltip]=\"label\"\n [tooltipDisabled]=\"!cutCustom.isTruncated()\"\n (click)=\"onTreeItemTextClick(node, $event)\">\n <ng-container\n [ngTemplateOutlet]=\"tpl\"\n [ngTemplateOutletContext]=\"{\n $implicit: node,\n field: aggregation()?.column,\n selected: treeItem.selected() ?? false,\n count: node.count,\n name: label,\n searchText: searchText(),\n level: node.$level ?? 0,\n expanded: node.$opened ?? false,\n hasChildren: node.hasChildren ?? false\n }\" />\n </span>\n } @else {\n <!-- ce span est essentiel pour le style -->\n @let appliesOnLabelClick = quickFilter() || applyOnSelect();\n <!-- Cut off, the label shows whole in a tooltip (`truncated` measures it); a label that fits has none. -->\n <span\n [class]=\"\n cn(\n 'grow truncate select-none',\n appliesOnLabelClick && 'hover:underline'\n )\n \"\n truncated\n #cut=\"truncated\"\n [tooltip]=\"label\"\n [tooltipDisabled]=\"!cut.isTruncated()\"\n (click)=\"onTreeItemTextClick(node, $event)\">\n @for (\n chunk of label | highlightWord: searchText() : 10;\n track $index\n ) {\n <span [class]=\"{ 'font-bold': chunk.match }\" aria-hidden=\"true\">{{\n chunk.text\n }}</span>\n }\n </span>\n @if (showCount() && node.count > 0) {\n <span class=\"ml-auto px-1 text-sm\" aria-hidden=\"true\">{{\n node.count\n }}</span>\n }\n }\n @let description = nodeDescription(node);\n @if (description) {\n <!-- `hidden`, not `sr-only`: read through aria-describedby only. A visually-hidden\n span would also be counted in the treeitem's name (computed from its content). -->\n <span hidden [id]=\"descriptionId(node.$path)\">{{ description }}</span>\n }\n </div>\n </li>\n @if (node.hasChildren) {\n <!-- No indentation here: it would shift the checkbox too. Depth is applied\n inside the row, after the checkbox, so every checkbox stays flush left. -->\n <!-- The <ul> sits INSIDE the deferred content, not around it: `ngTreeItemGroup` only\n instantiates its view when the node opens, so a <ul> declared outside stayed in the DOM,\n empty, for every collapsed node — a structural tag with nothing to structure (RGAA 8.9).\n The @if cannot move onto the <ul> either: the directive registers itself with its tree\n item on init and has to exist for as long as the item does. -->\n <ng-template\n ngTreeItemGroup\n [ownedBy]=\"treeItem\"\n #group=\"ngTreeItemGroup\">\n <ul role=\"group\">\n <ng-template\n [ngTemplateOutlet]=\"treeNodes\"\n [ngTemplateOutletContext]=\"{ nodes: node.items, parent: group }\" />\n </ul>\n </ng-template>\n }\n }\n</ng-template>\n",
|
|
27853
28466
|
dependencies: [
|
|
27854
28467
|
{
|
|
27855
28468
|
kind: "directive",
|
|
@@ -28069,7 +28682,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
28069
28682
|
AggregationLoadMoreSkeletonComponent
|
|
28070
28683
|
],
|
|
28071
28684
|
host: { "[class]": "cn(\"flex h-[inherit] max-h-[inherit] flex-col\", class(), (isMissing() || (hideWhenEmpty() && isHidden())) && \"hidden\")" },
|
|
28072
|
-
template: "@if (!aggregation()?.isTree) {\n <div role=\"alert\" class=\"p-2 text-sm text-red-500\">\n <triangle-alert-icon class=\"mr-1 shrink-0\" />\n The aggregationTree component does not support list aggregations. Please use\n the <Aggregation /> component instead.\n </div>\n}\n<aggregation-panel\n [id]=\"id()\"\n [collapsible]=\"collapsible()\"\n [collapsed]=\"collapsed()\"\n [isEmpty]=\"isEmpty()\"\n [isDate]=\"isDate()\"\n [aggregation]=\"aggregation()\"\n [showFiltersCount]=\"showFiltersCount()\"\n [filtersCount]=\"filtersCount()\"\n [hasFilters]=\"hasFilters()\"\n [selection]=\"selection()\"\n [isAllSelected]=\"isAllSelected()\"\n [headless]=\"headless()\"\n [selectedCount]=\"selected().length\"\n [(searchText)]=\"searchText\"\n [itemsLength]=\"items().length\"\n [hasMore]=\"aggregation()?.$hasMore ?? false\"\n [loading]=\"loadingMore()\"\n [searchedItemsLength]=\"searchedItems().length\"\n (cleared)=\"clear()\"\n (applied)=\"apply()\"\n (allSelected)=\"selectAll()\"\n (allUnselected)=\"unselectAll()\"\n (loadedMore)=\"loadMore()\">\n <!-- Relay the host's `label` projection into the panel's `label` slot. When the\n host projects nothing, the local <ng-content> falls back to the icon + display\n (mirrors AggregationPanel's own fallback), so the header is never empty. -->\n <ng-container ngProjectAs=\"label\">\n <ng-content select=\"label\">\n @let labelIcon = aggregation()?.icon;\n @if (labelIcon) {\n <FaIcon [faClass]=\"labelIcon\" class=\"mr-1 shrink-0\" />\n }\n <span class=\"grow truncate\">{{\n aggregation()?.display | syslang | transloco\n }}</span>\n </ng-content>\n </ng-container>\n <div class=\"min-h-0 w-full flex-1 scrollbar-thin overflow-auto pb-3\">\n <ul\n ngTree\n #tree=\"ngTree\"\n [multi]=\"true\"\n [(value)]=\"selected\"\n (valueChange)=\"onSelectionChange($event)\"\n /* Translated like the flat list's own label (aggregation-list.html): this was the raw\n `display`, so the accessible name of a whole facet came out in the source language.\n The pipes apply to the whole nullish chain, `|` being the loosest operator Angular\n parses — no parentheses needed. */\n [attr.aria-label]=\"aggregation()?.display ?? name() | syslang | transloco\"\n class=\"basic-tree m-1\">\n <ng-template\n [ngTemplateOutlet]=\"treeNodes\"\n [ngTemplateOutletContext]=\"{ nodes: nodes(), parent: tree }\" />\n </ul>\n @if (loadingMore() && searchedItems().length === 0) {\n <AggregationLoadMoreSkeleton variant=\"tree\" [showCount]=\"showCount()\" />\n }\n </div>\n</aggregation-panel>\n\n<ng-template #treeNodes let-nodes=\"nodes\" let-parent=\"parent\">\n @for (node of nodes; track node.$path) {\n @let label = node.display ?? node.value ?? \"\";\n <li\n ngTreeItem\n #treeItem=\"ngTreeItem\"\n [parent]=\"parent\"\n [value]=\"node.$path\"\n [label]=\"label\"\n [disabled]=\"node.disabled\"\n [(expanded)]=\"node.$opened\"\n (expandedChange)=\"onExpandedChange(node, $event)\"\n [attr.aria-label]=\"label\"\n [attr.aria-describedby]=\"\n nodeDescription(node) ? descriptionId(node.$path) : null\n \"\n [class]=\"cn('m-0 block w-full p-0', itemStateClass)\"\n (keydown.enter)=\"onTreeItemEnterKey(node, $event)\"\n (keydown.space)=\"onTreeItemSpaceKey(node, $event)\">\n <!-- 2. Ce div prend tout l'espace et intercepte tous les clics de la ligne. `ps-0.5`: the checkbox of a list option sits\n 2px further in than a tree's (the option has its own padding), so the checkboxes of the two facets, and the select-all of\n the bar of a popover, are on the same vertical line. -->\n <div\n class=\"flex w-full cursor-pointer items-center gap-2 py-0.5 ps-0.5 text-sm leading-6\"\n (click)=\"onTreeItemRowClick(node, $event)\"\n (mousedown)=\"$event.stopPropagation()\"\n (pointerdown)=\"$event.stopPropagation()\">\n @if (showCheckbox()) {\n <!-- `inert` keeps the native input out of the tab order and out of hit-testing:\n a role=\"treeitem\" admits no focusable descendant, and the click must reach\n the row handler above. Selection is announced by the <li>'s aria-selected. -->\n <!-- --spacing-xs: 0 kills the gap the checkbox keeps between its box and its\n (here empty) text block. Set as a custom property rather than a utility\n class: the CVA already sets gap-(--spacing-xs), and two same-specificity\n classes would be resolved by stylesheet order, not by us. -->\n <span\n inert\n aria-hidden=\"true\"\n class=\"inline-flex shrink-0\"\n style=\"--spacing-xs: 0\">\n <checkbox\n [checked]=\"treeItem.selected() || node.$selectedVisually || false\"\n [disabled]=\"node.disabled\"\n [class]=\"\n cn(\n 'pointer-events-none',\n (treeItem.selected() || node.$selectedVisually) &&\n 'selected-icon'\n )\n \" />\n </span>\n }\n <!-- Depth indent sits on the chevron, after the checkbox: the checkboxes of every\n level stay flush left (aligned with a flat list's), and since this adds no extra\n flex child, the step between consecutive levels is exactly one indent. -->\n @let depthIndent =\n \"calc(var(--agg-tree-indent, 0.5rem) * \" +\n (treeItem.level() - 1) +\n \")\";\n @if (node.hasChildren) {\n <!-- Expand/collapse affordance for the mouse only, hence a <span> and not a <button>:\n the keyboard already expands with ArrowRight / ArrowLeft, handled by ngTreeItem, and\n a role=\"treeitem\" admits no focusable descendant — so this was a <button> nobody\n could reach, already carrying aria-hidden and tabindex=\"-1\" to say so. Every audit\n reads it as a button with no accessible name (RGAA 7.1, 11.9). Same shape as the leaf\n branch below. -->\n <span\n aria-hidden=\"true\"\n (click)=\"toggleTreeNode(treeItem, $event)\"\n [style.marginInlineStart]=\"depthIndent\"\n class=\"inline-flex cursor-pointer items-center justify-center\">\n <ChevronRightIcon\n [class]=\"\n cn('expand-icon shrink-0', node.$opened && 'rotate-90')\n \" />\n <FolderIcon class=\"shrink-0\" />\n </span>\n } @else {\n <span\n aria-hidden=\"true\"\n [style.marginInlineStart]=\"depthIndent\"\n class=\"inline-flex items-center justify-center\">\n <!-- Reserves the chevron's own width (`.expand-icon` is a styling hook with\n no rule of its own), so a leaf lines up with its parent nodes. -->\n <span class=\"expand-icon w-4 shrink-0\"></span>\n <FileIcon class=\"size-4 shrink-0\" />\n </span>\n }\n @if (resolvedItemTemplate(); as tpl) {\n <!-- Cut off, the label shows whole in a tooltip; a label that fits needs none. -->\n <span\n class=\"grow truncate select-none\"\n truncated\n #cutCustom=\"truncated\"\n [tooltip]=\"label\"\n [tooltipDisabled]=\"!cutCustom.isTruncated()\"\n (click)=\"onTreeItemTextClick(node, $event)\">\n <ng-container\n [ngTemplateOutlet]=\"tpl\"\n [ngTemplateOutletContext]=\"{\n $implicit: node,\n field: aggregation()?.column,\n selected: treeItem.selected() ?? false,\n count: node.count,\n name: label,\n searchText: searchText(),\n level: node.$level ?? 0,\n expanded: node.$opened ?? false,\n hasChildren: node.hasChildren ?? false\n }\" />\n </span>\n } @else {\n <!-- ce span est essentiel pour le style -->\n @let appliesOnLabelClick = quickFilter() || applyOnSelect();\n <!-- Cut off, the label shows whole in a tooltip (`truncated` measures it); a label that fits has none. -->\n <span\n [class]=\"\n cn(\n 'grow truncate select-none',\n appliesOnLabelClick && 'hover:underline'\n )\n \"\n truncated\n #cut=\"truncated\"\n [tooltip]=\"label\"\n [tooltipDisabled]=\"!cut.isTruncated()\"\n (click)=\"onTreeItemTextClick(node, $event)\">\n @for (\n chunk of label | highlightWord: searchText() : 10;\n track $index\n ) {\n <span [class]=\"{ 'font-bold': chunk.match }\" aria-hidden=\"true\">{{\n chunk.text\n }}</span>\n }\n </span>\n @if (showCount() && node.count > 0) {\n <span class=\"ml-auto px-1 text-xs\" aria-hidden=\"true\">{{\n node.count\n }}</span>\n }\n }\n @let description = nodeDescription(node);\n @if (description) {\n <!-- `hidden`, not `sr-only`: read through aria-describedby only. A visually-hidden\n span would also be counted in the treeitem's name (computed from its content). -->\n <span hidden [id]=\"descriptionId(node.$path)\">{{ description }}</span>\n }\n </div>\n </li>\n @if (node.hasChildren) {\n <!-- No indentation here: it would shift the checkbox too. Depth is applied\n inside the row, after the checkbox, so every checkbox stays flush left. -->\n <!-- The <ul> sits INSIDE the deferred content, not around it: `ngTreeItemGroup` only\n instantiates its view when the node opens, so a <ul> declared outside stayed in the DOM,\n empty, for every collapsed node — a structural tag with nothing to structure (RGAA 8.9).\n The @if cannot move onto the <ul> either: the directive registers itself with its tree\n item on init and has to exist for as long as the item does. -->\n <ng-template\n ngTreeItemGroup\n [ownedBy]=\"treeItem\"\n #group=\"ngTreeItemGroup\">\n <ul role=\"group\">\n <ng-template\n [ngTemplateOutlet]=\"treeNodes\"\n [ngTemplateOutletContext]=\"{ nodes: node.items, parent: group }\" />\n </ul>\n </ng-template>\n }\n }\n</ng-template>\n"
|
|
28685
|
+
template: "@if (!aggregation()?.isTree) {\n <div role=\"alert\" class=\"p-2 text-sm text-red-500\">\n <triangle-alert-icon class=\"mr-1 shrink-0\" />\n The aggregationTree component does not support list aggregations. Please use\n the <Aggregation /> component instead.\n </div>\n}\n<aggregation-panel\n [id]=\"id()\"\n [collapsible]=\"collapsible()\"\n [collapsed]=\"collapsed()\"\n [isEmpty]=\"isEmpty()\"\n [isDate]=\"isDate()\"\n [aggregation]=\"aggregation()\"\n [showFiltersCount]=\"showFiltersCount()\"\n [filtersCount]=\"filtersCount()\"\n [hasFilters]=\"hasFilters()\"\n [selection]=\"selection()\"\n [isAllSelected]=\"isAllSelected()\"\n [headless]=\"headless()\"\n [selectedCount]=\"selected().length\"\n [(searchText)]=\"searchText\"\n [itemsLength]=\"items().length\"\n [hasMore]=\"aggregation()?.$hasMore ?? false\"\n [loading]=\"loadingMore()\"\n [searchedItemsLength]=\"searchedItems().length\"\n (cleared)=\"clear()\"\n (applied)=\"apply()\"\n (allSelected)=\"selectAll()\"\n (allUnselected)=\"unselectAll()\"\n (loadedMore)=\"loadMore()\">\n <!-- Relay the host's `label` projection into the panel's `label` slot. When the\n host projects nothing, the local <ng-content> falls back to the icon + display\n (mirrors AggregationPanel's own fallback), so the header is never empty. -->\n <ng-container ngProjectAs=\"label\">\n <ng-content select=\"label\">\n @let labelIcon = aggregation()?.icon;\n @if (labelIcon) {\n <FaIcon [faClass]=\"labelIcon\" class=\"mr-1 shrink-0\" />\n }\n <span class=\"grow truncate\">{{\n aggregation()?.display | syslang | transloco\n }}</span>\n </ng-content>\n </ng-container>\n <div class=\"min-h-0 w-full flex-1 scrollbar-thin overflow-auto pb-3\">\n <ul\n ngTree\n #tree=\"ngTree\"\n [multi]=\"true\"\n [(value)]=\"selected\"\n (valueChange)=\"onSelectionChange($event)\"\n /* Translated like the flat list's own label (aggregation-list.html): this was the raw\n `display`, so the accessible name of a whole facet came out in the source language.\n The pipes apply to the whole nullish chain, `|` being the loosest operator Angular\n parses — no parentheses needed. */\n [attr.aria-label]=\"aggregation()?.display ?? name() | syslang | transloco\"\n class=\"basic-tree m-1\">\n <ng-template\n [ngTemplateOutlet]=\"treeNodes\"\n [ngTemplateOutletContext]=\"{ nodes: nodes(), parent: tree }\" />\n </ul>\n @if (loadingMore() && searchedItems().length === 0) {\n <AggregationLoadMoreSkeleton variant=\"tree\" [showCount]=\"showCount()\" />\n }\n </div>\n</aggregation-panel>\n\n<ng-template #treeNodes let-nodes=\"nodes\" let-parent=\"parent\">\n @for (node of nodes; track node.$path) {\n @let label = node.display ?? node.value ?? \"\";\n <li\n ngTreeItem\n #treeItem=\"ngTreeItem\"\n [parent]=\"parent\"\n [value]=\"node.$path\"\n [label]=\"label\"\n [disabled]=\"node.disabled\"\n [(expanded)]=\"node.$opened\"\n (expandedChange)=\"onExpandedChange(node, $event)\"\n [attr.aria-label]=\"label\"\n [attr.aria-describedby]=\"\n nodeDescription(node) ? descriptionId(node.$path) : null\n \"\n [class]=\"cn('m-0 block w-full p-0', itemStateClass)\"\n (keydown.enter)=\"onTreeItemEnterKey(node, $event)\"\n (keydown.space)=\"onTreeItemSpaceKey(node, $event)\">\n <!-- 2. Ce div prend tout l'espace et intercepte tous les clics de la ligne. `ps-0.5`: the checkbox of a list option sits\n 2px further in than a tree's (the option has its own padding), so the checkboxes of the two facets, and the select-all of\n the bar of a popover, are on the same vertical line. -->\n <div\n class=\"flex w-full cursor-pointer items-center gap-2 py-1.5 ps-0.5 text-base leading-6\"\n (click)=\"onTreeItemRowClick(node, $event)\"\n (mousedown)=\"$event.stopPropagation()\"\n (pointerdown)=\"$event.stopPropagation()\">\n @if (showCheckbox()) {\n <!-- `inert` keeps the native input out of the tab order and out of hit-testing:\n a role=\"treeitem\" admits no focusable descendant, and the click must reach\n the row handler above. Selection is announced by the <li>'s aria-selected. -->\n <!-- --spacing-xs: 0 kills the gap the checkbox keeps between its box and its\n (here empty) text block. Set as a custom property rather than a utility\n class: the CVA already sets gap-(--spacing-xs), and two same-specificity\n classes would be resolved by stylesheet order, not by us. -->\n <span\n inert\n aria-hidden=\"true\"\n class=\"inline-flex shrink-0\"\n style=\"--spacing-xs: 0\">\n <checkbox\n [checked]=\"treeItem.selected() || node.$selectedVisually || false\"\n [disabled]=\"node.disabled\"\n [class]=\"\n cn(\n 'pointer-events-none',\n (treeItem.selected() || node.$selectedVisually) &&\n 'selected-icon'\n )\n \" />\n </span>\n }\n <!-- Depth indent sits on the chevron, after the checkbox: the checkboxes of every\n level stay flush left (aligned with a flat list's), and since this adds no extra\n flex child, the step between consecutive levels is exactly one indent. -->\n @let depthIndent =\n \"calc(var(--agg-tree-indent, 0.5rem) * \" +\n (treeItem.level() - 1) +\n \")\";\n @if (node.hasChildren) {\n <!-- Expand/collapse affordance for the mouse only, hence a <span> and not a <button>:\n the keyboard already expands with ArrowRight / ArrowLeft, handled by ngTreeItem, and\n a role=\"treeitem\" admits no focusable descendant — so this was a <button> nobody\n could reach, already carrying aria-hidden and tabindex=\"-1\" to say so. Every audit\n reads it as a button with no accessible name (RGAA 7.1, 11.9). Same shape as the leaf\n branch below. -->\n <span\n aria-hidden=\"true\"\n (click)=\"toggleTreeNode(treeItem, $event)\"\n [style.marginInlineStart]=\"depthIndent\"\n class=\"inline-flex cursor-pointer items-center justify-center\">\n <ChevronRightIcon\n [class]=\"\n cn('expand-icon shrink-0', node.$opened && 'rotate-90')\n \" />\n <FolderIcon class=\"shrink-0\" />\n </span>\n } @else {\n <span\n aria-hidden=\"true\"\n [style.marginInlineStart]=\"depthIndent\"\n class=\"inline-flex items-center justify-center\">\n <!-- Reserves the chevron's own width (`.expand-icon` is a styling hook with\n no rule of its own), so a leaf lines up with its parent nodes. -->\n <span class=\"expand-icon w-4 shrink-0\"></span>\n <FileIcon class=\"size-4 shrink-0\" />\n </span>\n }\n @if (resolvedItemTemplate(); as tpl) {\n <!-- Cut off, the label shows whole in a tooltip; a label that fits needs none. -->\n <span\n class=\"grow truncate select-none\"\n truncated\n #cutCustom=\"truncated\"\n [tooltip]=\"label\"\n [tooltipDisabled]=\"!cutCustom.isTruncated()\"\n (click)=\"onTreeItemTextClick(node, $event)\">\n <ng-container\n [ngTemplateOutlet]=\"tpl\"\n [ngTemplateOutletContext]=\"{\n $implicit: node,\n field: aggregation()?.column,\n selected: treeItem.selected() ?? false,\n count: node.count,\n name: label,\n searchText: searchText(),\n level: node.$level ?? 0,\n expanded: node.$opened ?? false,\n hasChildren: node.hasChildren ?? false\n }\" />\n </span>\n } @else {\n <!-- ce span est essentiel pour le style -->\n @let appliesOnLabelClick = quickFilter() || applyOnSelect();\n <!-- Cut off, the label shows whole in a tooltip (`truncated` measures it); a label that fits has none. -->\n <span\n [class]=\"\n cn(\n 'grow truncate select-none',\n appliesOnLabelClick && 'hover:underline'\n )\n \"\n truncated\n #cut=\"truncated\"\n [tooltip]=\"label\"\n [tooltipDisabled]=\"!cut.isTruncated()\"\n (click)=\"onTreeItemTextClick(node, $event)\">\n @for (\n chunk of label | highlightWord: searchText() : 10;\n track $index\n ) {\n <span [class]=\"{ 'font-bold': chunk.match }\" aria-hidden=\"true\">{{\n chunk.text\n }}</span>\n }\n </span>\n @if (showCount() && node.count > 0) {\n <span class=\"ml-auto px-1 text-sm\" aria-hidden=\"true\">{{\n node.count\n }}</span>\n }\n }\n @let description = nodeDescription(node);\n @if (description) {\n <!-- `hidden`, not `sr-only`: read through aria-describedby only. A visually-hidden\n span would also be counted in the treeitem's name (computed from its content). -->\n <span hidden [id]=\"descriptionId(node.$path)\">{{ description }}</span>\n }\n </div>\n </li>\n @if (node.hasChildren) {\n <!-- No indentation here: it would shift the checkbox too. Depth is applied\n inside the row, after the checkbox, so every checkbox stays flush left. -->\n <!-- The <ul> sits INSIDE the deferred content, not around it: `ngTreeItemGroup` only\n instantiates its view when the node opens, so a <ul> declared outside stayed in the DOM,\n empty, for every collapsed node — a structural tag with nothing to structure (RGAA 8.9).\n The @if cannot move onto the <ul> either: the directive registers itself with its tree\n item on init and has to exist for as long as the item does. -->\n <ng-template\n ngTreeItemGroup\n [ownedBy]=\"treeItem\"\n #group=\"ngTreeItemGroup\">\n <ul role=\"group\">\n <ng-template\n [ngTemplateOutlet]=\"treeNodes\"\n [ngTemplateOutletContext]=\"{ nodes: node.items, parent: group }\" />\n </ul>\n </ng-template>\n }\n }\n</ng-template>\n"
|
|
28073
28686
|
}]
|
|
28074
28687
|
}],
|
|
28075
28688
|
ctorParameters: () => [],
|
|
@@ -28263,6 +28876,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
28263
28876
|
}]
|
|
28264
28877
|
}
|
|
28265
28878
|
});
|
|
28879
|
+
const ROW_ESTIMATE = 36;
|
|
28266
28880
|
let nextDescriptionScope = 0;
|
|
28267
28881
|
var AggregationListComponent = class AggregationListComponent {
|
|
28268
28882
|
cn = cn;
|
|
@@ -28376,7 +28990,7 @@ var AggregationListComponent = class AggregationListComponent {
|
|
|
28376
28990
|
cdr = inject(ChangeDetectorRef);
|
|
28377
28991
|
virtualizer = injectVirtualizer(() => ({
|
|
28378
28992
|
count: this.items().length,
|
|
28379
|
-
estimateSize: () =>
|
|
28993
|
+
estimateSize: () => ROW_ESTIMATE,
|
|
28380
28994
|
gap: 2,
|
|
28381
28995
|
scrollElement: this.scrollElement()
|
|
28382
28996
|
}));
|
|
@@ -28781,7 +29395,7 @@ var AggregationListComponent = class AggregationListComponent {
|
|
|
28781
29395
|
}
|
|
28782
29396
|
],
|
|
28783
29397
|
ngImport: i0,
|
|
28784
|
-
template: "@if (aggregation()?.isTree) {\n <div class=\"p-2 text-sm text-red-500\">\n <triangle-alert-icon class=\"mr-1 shrink-0\" />\n The aggregation component no longer supports tree aggregations. Please use\n the <AggregationTree /> component instead.\n </div>\n}\n<aggregation-panel\n [id]=\"id()\"\n [collapsible]=\"collapsible()\"\n [collapsed]=\"collapsed()\"\n [isEmpty]=\"isEmpty()\"\n [isDate]=\"isDate()\"\n [aggregation]=\"aggregation()\"\n [showFiltersCount]=\"showFiltersCount()\"\n [filtersCount]=\"filtersCount()\"\n [hasFilters]=\"hasFilters()\"\n [selection]=\"selection()\"\n [isAllSelected]=\"isAllSelected()\"\n [headless]=\"headless()\"\n [selectedCount]=\"selectedValues().length\"\n [(searchText)]=\"searchText\"\n [itemsLength]=\"items().length\"\n [hasMore]=\"aggregation()?.$hasMore ?? false\"\n [loading]=\"loadingMore()\"\n [searchedItemsLength]=\"searchedItems().length\"\n (cleared)=\"clear()\"\n (applied)=\"apply()\"\n (allSelected)=\"selectAll()\"\n (allUnselected)=\"unselectAll()\"\n (loadedMore)=\"loadMore()\">\n <!-- Relay the host's `label` projection into the panel's `label` slot. When the\n host projects nothing, the local <ng-content> falls back to the icon + display\n (mirrors AggregationPanel's own fallback), so the header is never empty. -->\n <ng-container ngProjectAs=\"label\">\n <ng-content select=\"label\">\n @let labelIcon = aggregation()?.icon;\n @if (labelIcon) {\n <FaIcon [faClass]=\"labelIcon\" class=\"mr-1 shrink-0\" />\n }\n <span class=\"grow truncate\">{{\n aggregation()?.display | syslang | transloco\n }}</span>\n </ng-content>\n </ng-container>\n <div\n #scrollElement\n class=\"min-h-0 w-full flex-1 scrollbar-thin overflow-auto px-1 pt-1 pb-3\">\n <div\n ngListbox\n multi\n selectionMode=\"explicit\"\n [value]=\"selectedValues()\"\n (valueChange)=\"syncListboxValue($event)\"\n [wrap]=\"false\"\n [softDisabled]=\"false\"\n class=\"relative w-full\"\n [style.height]=\"virtualizer.getTotalSize() + 'px'\"\n role=\"listbox\"\n aria-multiselectable=\"true\"\n [attr.aria-label]=\"aggregation()?.display | syslang | transloco\">\n @for (vItem of virtualizer.getVirtualItems(); track vItem.index) {\n @let item = items()[vItem.index];\n @let description = rowDescription(item);\n <div\n #row\n ngOption\n [attr.aria-describedby]=\"\n description ? descriptionId(vItem.index) : null\n \"\n [attr.data-index]=\"vItem.index\"\n [value]=\"item.value\"\n [disabled]=\"item.count === 0\"\n [class]=\"cn('absolute w-full', menuItemBaseClass)\"\n (keydown.enter)=\"onOptionEnterKey(item, $event)\"\n (keydown.space)=\"\n onListItemRowClick(item, $event); $event.preventDefault()\n \"\n [style.transform]=\"'translateY(' + vItem.start + 'px)'\"\n role=\"option\">\n <ng-container\n [ngTemplateOutlet]=\"listItemTpl\"\n [ngTemplateOutletContext]=\"{\n $implicit: item,\n field: aggregation()?.column\n }\">\n </ng-container>\n @if (description) {\n <!-- `hidden`, not `sr-only`: read through aria-describedby only. A visually-hidden\n span would also be counted in the option's name (computed from its content). -->\n <span hidden [id]=\"descriptionId(vItem.index)\">{{\n description\n }}</span>\n }\n </div>\n }\n </div>\n @if (loadingMore() && searchedItems().length === 0) {\n <AggregationLoadMoreSkeleton [showCount]=\"showCount()\" />\n }\n </div>\n</aggregation-panel>\n\n<ng-template #listItemTpl let-item let-field=\"field\">\n <a\n [attr.aria-label]=\"listItemName(item) | syslang\"\n [class]=\"\n cn(\n '-m-1.5 flex min-w-0 grow items-center gap-2 leading-6',\n item.count === 0 && 'pointer-events-none text-(--font-neutral-muted)'\n )\n \"\n (click)=\"onListItemRowClick(item, $event)\">\n @if (showCheckbox()) {\n <!-- `inert` keeps the native input out of the tab order and out of hit-testing:\n keyboard navigation belongs to ngListbox/ngOption (roving tabindex), the\n selection is already exposed by role=\"option\" + aria-selected, and the click\n must reach the <a> above. Same treatment as the tree.\n --spacing-xs: 0 kills the gap the checkbox keeps between its box and its\n (here empty) text block. -->\n <span\n inert\n aria-hidden=\"true\"\n class=\"inline-flex shrink-0\"\n style=\"--spacing-xs: 0\">\n <checkbox\n [checked]=\"item.$selected ?? false\"\n [disabled]=\"item.count === 0\"\n [class]=\"'pointer-events-none'\" />\n </span>\n }\n @if (resolvedItemTemplate(); as tpl) {\n <ng-container\n [ngTemplateOutlet]=\"tpl\"\n [ngTemplateOutletContext]=\"{\n $implicit: item,\n field: field,\n selected: item.$selected ?? false,\n count: item.count,\n name: (listItemName(item) | syslang) ?? '',\n searchText: searchText()\n }\" />\n } @else {\n @let icon = $any(item).icon;\n @if (icon) {\n <FaIcon\n [faClass]=\"icon\"\n class=\"shrink-0 self-center justify-self-center\" />\n }\n @let appliesOnLabelClick = quickFilter() || applyOnSelect();\n <!-- Cut off, the label shows whole in a tooltip (`truncated` measures it); a label that fits has none. -->\n <span\n [class]=\"\n cn(\n 'line-clamp-1 cursor-pointer break-all text-ellipsis select-none',\n appliesOnLabelClick && 'hover:underline'\n )\n \"\n truncated\n #cut=\"truncated\"\n [tooltip]=\"listItemName(item) | syslang\"\n [tooltipDisabled]=\"!cut.isTruncated()\"\n (click)=\"onListItemTextClick(item, $event)\">\n @for (\n chunk of (listItemName(item) | syslang) ?? \"\"\n | highlightWord: searchText() : 10;\n track $index\n ) {\n <span [class]=\"{ 'font-bold': chunk.match }\" aria-hidden=\"true\">{{\n chunk.text\n }}</span>\n }\n </span>\n @if (showCount() && item.count > 0) {\n <span class=\"ml-auto px-1 text-
|
|
29398
|
+
template: "@if (aggregation()?.isTree) {\n <div class=\"p-2 text-sm text-red-500\">\n <triangle-alert-icon class=\"mr-1 shrink-0\" />\n The aggregation component no longer supports tree aggregations. Please use\n the <AggregationTree /> component instead.\n </div>\n}\n<aggregation-panel\n [id]=\"id()\"\n [collapsible]=\"collapsible()\"\n [collapsed]=\"collapsed()\"\n [isEmpty]=\"isEmpty()\"\n [isDate]=\"isDate()\"\n [aggregation]=\"aggregation()\"\n [showFiltersCount]=\"showFiltersCount()\"\n [filtersCount]=\"filtersCount()\"\n [hasFilters]=\"hasFilters()\"\n [selection]=\"selection()\"\n [isAllSelected]=\"isAllSelected()\"\n [headless]=\"headless()\"\n [selectedCount]=\"selectedValues().length\"\n [(searchText)]=\"searchText\"\n [itemsLength]=\"items().length\"\n [hasMore]=\"aggregation()?.$hasMore ?? false\"\n [loading]=\"loadingMore()\"\n [searchedItemsLength]=\"searchedItems().length\"\n (cleared)=\"clear()\"\n (applied)=\"apply()\"\n (allSelected)=\"selectAll()\"\n (allUnselected)=\"unselectAll()\"\n (loadedMore)=\"loadMore()\">\n <!-- Relay the host's `label` projection into the panel's `label` slot. When the\n host projects nothing, the local <ng-content> falls back to the icon + display\n (mirrors AggregationPanel's own fallback), so the header is never empty. -->\n <ng-container ngProjectAs=\"label\">\n <ng-content select=\"label\">\n @let labelIcon = aggregation()?.icon;\n @if (labelIcon) {\n <FaIcon [faClass]=\"labelIcon\" class=\"mr-1 shrink-0\" />\n }\n <span class=\"grow truncate\">{{\n aggregation()?.display | syslang | transloco\n }}</span>\n </ng-content>\n </ng-container>\n <div\n #scrollElement\n class=\"min-h-0 w-full flex-1 scrollbar-thin overflow-auto px-1 pt-1 pb-3\">\n <div\n ngListbox\n multi\n selectionMode=\"explicit\"\n [value]=\"selectedValues()\"\n (valueChange)=\"syncListboxValue($event)\"\n [wrap]=\"false\"\n [softDisabled]=\"false\"\n class=\"relative w-full\"\n [style.height]=\"virtualizer.getTotalSize() + 'px'\"\n role=\"listbox\"\n aria-multiselectable=\"true\"\n [attr.aria-label]=\"aggregation()?.display | syslang | transloco\">\n @for (vItem of virtualizer.getVirtualItems(); track vItem.index) {\n @let item = items()[vItem.index];\n @let description = rowDescription(item);\n <div\n #row\n ngOption\n [attr.aria-describedby]=\"\n description ? descriptionId(vItem.index) : null\n \"\n [attr.data-index]=\"vItem.index\"\n [value]=\"item.value\"\n [disabled]=\"item.count === 0\"\n [class]=\"cn('absolute w-full', menuItemBaseClass)\"\n (keydown.enter)=\"onOptionEnterKey(item, $event)\"\n (keydown.space)=\"\n onListItemRowClick(item, $event); $event.preventDefault()\n \"\n [style.transform]=\"'translateY(' + vItem.start + 'px)'\"\n role=\"option\">\n <ng-container\n [ngTemplateOutlet]=\"listItemTpl\"\n [ngTemplateOutletContext]=\"{\n $implicit: item,\n field: aggregation()?.column\n }\">\n </ng-container>\n @if (description) {\n <!-- `hidden`, not `sr-only`: read through aria-describedby only. A visually-hidden\n span would also be counted in the option's name (computed from its content). -->\n <span hidden [id]=\"descriptionId(vItem.index)\">{{\n description\n }}</span>\n }\n </div>\n }\n </div>\n @if (loadingMore() && searchedItems().length === 0) {\n <AggregationLoadMoreSkeleton [showCount]=\"showCount()\" />\n }\n </div>\n</aggregation-panel>\n\n<ng-template #listItemTpl let-item let-field=\"field\">\n <a\n [attr.aria-label]=\"listItemName(item) | syslang\"\n [class]=\"\n cn(\n '-m-1.5 flex min-h-8 min-w-0 grow items-center gap-2 text-base leading-6',\n item.count === 0 && 'pointer-events-none text-(--font-neutral-muted)'\n )\n \"\n (click)=\"onListItemRowClick(item, $event)\">\n @if (showCheckbox()) {\n <!-- `inert` keeps the native input out of the tab order and out of hit-testing:\n keyboard navigation belongs to ngListbox/ngOption (roving tabindex), the\n selection is already exposed by role=\"option\" + aria-selected, and the click\n must reach the <a> above. Same treatment as the tree.\n --spacing-xs: 0 kills the gap the checkbox keeps between its box and its\n (here empty) text block. -->\n <span\n inert\n aria-hidden=\"true\"\n class=\"inline-flex shrink-0\"\n style=\"--spacing-xs: 0\">\n <checkbox\n [checked]=\"item.$selected ?? false\"\n [disabled]=\"item.count === 0\"\n [class]=\"'pointer-events-none'\" />\n </span>\n }\n @if (resolvedItemTemplate(); as tpl) {\n <ng-container\n [ngTemplateOutlet]=\"tpl\"\n [ngTemplateOutletContext]=\"{\n $implicit: item,\n field: field,\n selected: item.$selected ?? false,\n count: item.count,\n name: (listItemName(item) | syslang) ?? '',\n searchText: searchText()\n }\" />\n } @else {\n @let icon = $any(item).icon;\n @if (icon) {\n <FaIcon\n [faClass]=\"icon\"\n class=\"shrink-0 self-center justify-self-center\" />\n }\n @let appliesOnLabelClick = quickFilter() || applyOnSelect();\n <!-- Cut off, the label shows whole in a tooltip (`truncated` measures it); a label that fits has none. -->\n <span\n [class]=\"\n cn(\n 'line-clamp-1 cursor-pointer break-all text-ellipsis select-none',\n appliesOnLabelClick && 'hover:underline'\n )\n \"\n truncated\n #cut=\"truncated\"\n [tooltip]=\"listItemName(item) | syslang\"\n [tooltipDisabled]=\"!cut.isTruncated()\"\n (click)=\"onListItemTextClick(item, $event)\">\n @for (\n chunk of (listItemName(item) | syslang) ?? \"\"\n | highlightWord: searchText() : 10;\n track $index\n ) {\n <span [class]=\"{ 'font-bold': chunk.match }\" aria-hidden=\"true\">{{\n chunk.text\n }}</span>\n }\n </span>\n @if (showCount() && item.count > 0) {\n <span class=\"ml-auto px-1 text-sm empty:hidden\" aria-hidden=\"true\">{{\n item.count\n }}</span>\n }\n }\n </a>\n</ng-template>\n",
|
|
28785
29399
|
dependencies: [
|
|
28786
29400
|
{
|
|
28787
29401
|
kind: "directive",
|
|
@@ -28970,7 +29584,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
28970
29584
|
"[class]": "cn(\"flex h-[inherit] max-h-[inherit] flex-col\", class(), (isMissing() || (hideWhenEmpty() && isEmpty())) && \"hidden\")",
|
|
28971
29585
|
"(keydown.enter)": "onHostEnterKey($event)"
|
|
28972
29586
|
},
|
|
28973
|
-
template: "@if (aggregation()?.isTree) {\n <div class=\"p-2 text-sm text-red-500\">\n <triangle-alert-icon class=\"mr-1 shrink-0\" />\n The aggregation component no longer supports tree aggregations. Please use\n the <AggregationTree /> component instead.\n </div>\n}\n<aggregation-panel\n [id]=\"id()\"\n [collapsible]=\"collapsible()\"\n [collapsed]=\"collapsed()\"\n [isEmpty]=\"isEmpty()\"\n [isDate]=\"isDate()\"\n [aggregation]=\"aggregation()\"\n [showFiltersCount]=\"showFiltersCount()\"\n [filtersCount]=\"filtersCount()\"\n [hasFilters]=\"hasFilters()\"\n [selection]=\"selection()\"\n [isAllSelected]=\"isAllSelected()\"\n [headless]=\"headless()\"\n [selectedCount]=\"selectedValues().length\"\n [(searchText)]=\"searchText\"\n [itemsLength]=\"items().length\"\n [hasMore]=\"aggregation()?.$hasMore ?? false\"\n [loading]=\"loadingMore()\"\n [searchedItemsLength]=\"searchedItems().length\"\n (cleared)=\"clear()\"\n (applied)=\"apply()\"\n (allSelected)=\"selectAll()\"\n (allUnselected)=\"unselectAll()\"\n (loadedMore)=\"loadMore()\">\n <!-- Relay the host's `label` projection into the panel's `label` slot. When the\n host projects nothing, the local <ng-content> falls back to the icon + display\n (mirrors AggregationPanel's own fallback), so the header is never empty. -->\n <ng-container ngProjectAs=\"label\">\n <ng-content select=\"label\">\n @let labelIcon = aggregation()?.icon;\n @if (labelIcon) {\n <FaIcon [faClass]=\"labelIcon\" class=\"mr-1 shrink-0\" />\n }\n <span class=\"grow truncate\">{{\n aggregation()?.display | syslang | transloco\n }}</span>\n </ng-content>\n </ng-container>\n <div\n #scrollElement\n class=\"min-h-0 w-full flex-1 scrollbar-thin overflow-auto px-1 pt-1 pb-3\">\n <div\n ngListbox\n multi\n selectionMode=\"explicit\"\n [value]=\"selectedValues()\"\n (valueChange)=\"syncListboxValue($event)\"\n [wrap]=\"false\"\n [softDisabled]=\"false\"\n class=\"relative w-full\"\n [style.height]=\"virtualizer.getTotalSize() + 'px'\"\n role=\"listbox\"\n aria-multiselectable=\"true\"\n [attr.aria-label]=\"aggregation()?.display | syslang | transloco\">\n @for (vItem of virtualizer.getVirtualItems(); track vItem.index) {\n @let item = items()[vItem.index];\n @let description = rowDescription(item);\n <div\n #row\n ngOption\n [attr.aria-describedby]=\"\n description ? descriptionId(vItem.index) : null\n \"\n [attr.data-index]=\"vItem.index\"\n [value]=\"item.value\"\n [disabled]=\"item.count === 0\"\n [class]=\"cn('absolute w-full', menuItemBaseClass)\"\n (keydown.enter)=\"onOptionEnterKey(item, $event)\"\n (keydown.space)=\"\n onListItemRowClick(item, $event); $event.preventDefault()\n \"\n [style.transform]=\"'translateY(' + vItem.start + 'px)'\"\n role=\"option\">\n <ng-container\n [ngTemplateOutlet]=\"listItemTpl\"\n [ngTemplateOutletContext]=\"{\n $implicit: item,\n field: aggregation()?.column\n }\">\n </ng-container>\n @if (description) {\n <!-- `hidden`, not `sr-only`: read through aria-describedby only. A visually-hidden\n span would also be counted in the option's name (computed from its content). -->\n <span hidden [id]=\"descriptionId(vItem.index)\">{{\n description\n }}</span>\n }\n </div>\n }\n </div>\n @if (loadingMore() && searchedItems().length === 0) {\n <AggregationLoadMoreSkeleton [showCount]=\"showCount()\" />\n }\n </div>\n</aggregation-panel>\n\n<ng-template #listItemTpl let-item let-field=\"field\">\n <a\n [attr.aria-label]=\"listItemName(item) | syslang\"\n [class]=\"\n cn(\n '-m-1.5 flex min-w-0 grow items-center gap-2 leading-6',\n item.count === 0 && 'pointer-events-none text-(--font-neutral-muted)'\n )\n \"\n (click)=\"onListItemRowClick(item, $event)\">\n @if (showCheckbox()) {\n <!-- `inert` keeps the native input out of the tab order and out of hit-testing:\n keyboard navigation belongs to ngListbox/ngOption (roving tabindex), the\n selection is already exposed by role=\"option\" + aria-selected, and the click\n must reach the <a> above. Same treatment as the tree.\n --spacing-xs: 0 kills the gap the checkbox keeps between its box and its\n (here empty) text block. -->\n <span\n inert\n aria-hidden=\"true\"\n class=\"inline-flex shrink-0\"\n style=\"--spacing-xs: 0\">\n <checkbox\n [checked]=\"item.$selected ?? false\"\n [disabled]=\"item.count === 0\"\n [class]=\"'pointer-events-none'\" />\n </span>\n }\n @if (resolvedItemTemplate(); as tpl) {\n <ng-container\n [ngTemplateOutlet]=\"tpl\"\n [ngTemplateOutletContext]=\"{\n $implicit: item,\n field: field,\n selected: item.$selected ?? false,\n count: item.count,\n name: (listItemName(item) | syslang) ?? '',\n searchText: searchText()\n }\" />\n } @else {\n @let icon = $any(item).icon;\n @if (icon) {\n <FaIcon\n [faClass]=\"icon\"\n class=\"shrink-0 self-center justify-self-center\" />\n }\n @let appliesOnLabelClick = quickFilter() || applyOnSelect();\n <!-- Cut off, the label shows whole in a tooltip (`truncated` measures it); a label that fits has none. -->\n <span\n [class]=\"\n cn(\n 'line-clamp-1 cursor-pointer break-all text-ellipsis select-none',\n appliesOnLabelClick && 'hover:underline'\n )\n \"\n truncated\n #cut=\"truncated\"\n [tooltip]=\"listItemName(item) | syslang\"\n [tooltipDisabled]=\"!cut.isTruncated()\"\n (click)=\"onListItemTextClick(item, $event)\">\n @for (\n chunk of (listItemName(item) | syslang) ?? \"\"\n | highlightWord: searchText() : 10;\n track $index\n ) {\n <span [class]=\"{ 'font-bold': chunk.match }\" aria-hidden=\"true\">{{\n chunk.text\n }}</span>\n }\n </span>\n @if (showCount() && item.count > 0) {\n <span class=\"ml-auto px-1 text-
|
|
29587
|
+
template: "@if (aggregation()?.isTree) {\n <div class=\"p-2 text-sm text-red-500\">\n <triangle-alert-icon class=\"mr-1 shrink-0\" />\n The aggregation component no longer supports tree aggregations. Please use\n the <AggregationTree /> component instead.\n </div>\n}\n<aggregation-panel\n [id]=\"id()\"\n [collapsible]=\"collapsible()\"\n [collapsed]=\"collapsed()\"\n [isEmpty]=\"isEmpty()\"\n [isDate]=\"isDate()\"\n [aggregation]=\"aggregation()\"\n [showFiltersCount]=\"showFiltersCount()\"\n [filtersCount]=\"filtersCount()\"\n [hasFilters]=\"hasFilters()\"\n [selection]=\"selection()\"\n [isAllSelected]=\"isAllSelected()\"\n [headless]=\"headless()\"\n [selectedCount]=\"selectedValues().length\"\n [(searchText)]=\"searchText\"\n [itemsLength]=\"items().length\"\n [hasMore]=\"aggregation()?.$hasMore ?? false\"\n [loading]=\"loadingMore()\"\n [searchedItemsLength]=\"searchedItems().length\"\n (cleared)=\"clear()\"\n (applied)=\"apply()\"\n (allSelected)=\"selectAll()\"\n (allUnselected)=\"unselectAll()\"\n (loadedMore)=\"loadMore()\">\n <!-- Relay the host's `label` projection into the panel's `label` slot. When the\n host projects nothing, the local <ng-content> falls back to the icon + display\n (mirrors AggregationPanel's own fallback), so the header is never empty. -->\n <ng-container ngProjectAs=\"label\">\n <ng-content select=\"label\">\n @let labelIcon = aggregation()?.icon;\n @if (labelIcon) {\n <FaIcon [faClass]=\"labelIcon\" class=\"mr-1 shrink-0\" />\n }\n <span class=\"grow truncate\">{{\n aggregation()?.display | syslang | transloco\n }}</span>\n </ng-content>\n </ng-container>\n <div\n #scrollElement\n class=\"min-h-0 w-full flex-1 scrollbar-thin overflow-auto px-1 pt-1 pb-3\">\n <div\n ngListbox\n multi\n selectionMode=\"explicit\"\n [value]=\"selectedValues()\"\n (valueChange)=\"syncListboxValue($event)\"\n [wrap]=\"false\"\n [softDisabled]=\"false\"\n class=\"relative w-full\"\n [style.height]=\"virtualizer.getTotalSize() + 'px'\"\n role=\"listbox\"\n aria-multiselectable=\"true\"\n [attr.aria-label]=\"aggregation()?.display | syslang | transloco\">\n @for (vItem of virtualizer.getVirtualItems(); track vItem.index) {\n @let item = items()[vItem.index];\n @let description = rowDescription(item);\n <div\n #row\n ngOption\n [attr.aria-describedby]=\"\n description ? descriptionId(vItem.index) : null\n \"\n [attr.data-index]=\"vItem.index\"\n [value]=\"item.value\"\n [disabled]=\"item.count === 0\"\n [class]=\"cn('absolute w-full', menuItemBaseClass)\"\n (keydown.enter)=\"onOptionEnterKey(item, $event)\"\n (keydown.space)=\"\n onListItemRowClick(item, $event); $event.preventDefault()\n \"\n [style.transform]=\"'translateY(' + vItem.start + 'px)'\"\n role=\"option\">\n <ng-container\n [ngTemplateOutlet]=\"listItemTpl\"\n [ngTemplateOutletContext]=\"{\n $implicit: item,\n field: aggregation()?.column\n }\">\n </ng-container>\n @if (description) {\n <!-- `hidden`, not `sr-only`: read through aria-describedby only. A visually-hidden\n span would also be counted in the option's name (computed from its content). -->\n <span hidden [id]=\"descriptionId(vItem.index)\">{{\n description\n }}</span>\n }\n </div>\n }\n </div>\n @if (loadingMore() && searchedItems().length === 0) {\n <AggregationLoadMoreSkeleton [showCount]=\"showCount()\" />\n }\n </div>\n</aggregation-panel>\n\n<ng-template #listItemTpl let-item let-field=\"field\">\n <a\n [attr.aria-label]=\"listItemName(item) | syslang\"\n [class]=\"\n cn(\n '-m-1.5 flex min-h-8 min-w-0 grow items-center gap-2 text-base leading-6',\n item.count === 0 && 'pointer-events-none text-(--font-neutral-muted)'\n )\n \"\n (click)=\"onListItemRowClick(item, $event)\">\n @if (showCheckbox()) {\n <!-- `inert` keeps the native input out of the tab order and out of hit-testing:\n keyboard navigation belongs to ngListbox/ngOption (roving tabindex), the\n selection is already exposed by role=\"option\" + aria-selected, and the click\n must reach the <a> above. Same treatment as the tree.\n --spacing-xs: 0 kills the gap the checkbox keeps between its box and its\n (here empty) text block. -->\n <span\n inert\n aria-hidden=\"true\"\n class=\"inline-flex shrink-0\"\n style=\"--spacing-xs: 0\">\n <checkbox\n [checked]=\"item.$selected ?? false\"\n [disabled]=\"item.count === 0\"\n [class]=\"'pointer-events-none'\" />\n </span>\n }\n @if (resolvedItemTemplate(); as tpl) {\n <ng-container\n [ngTemplateOutlet]=\"tpl\"\n [ngTemplateOutletContext]=\"{\n $implicit: item,\n field: field,\n selected: item.$selected ?? false,\n count: item.count,\n name: (listItemName(item) | syslang) ?? '',\n searchText: searchText()\n }\" />\n } @else {\n @let icon = $any(item).icon;\n @if (icon) {\n <FaIcon\n [faClass]=\"icon\"\n class=\"shrink-0 self-center justify-self-center\" />\n }\n @let appliesOnLabelClick = quickFilter() || applyOnSelect();\n <!-- Cut off, the label shows whole in a tooltip (`truncated` measures it); a label that fits has none. -->\n <span\n [class]=\"\n cn(\n 'line-clamp-1 cursor-pointer break-all text-ellipsis select-none',\n appliesOnLabelClick && 'hover:underline'\n )\n \"\n truncated\n #cut=\"truncated\"\n [tooltip]=\"listItemName(item) | syslang\"\n [tooltipDisabled]=\"!cut.isTruncated()\"\n (click)=\"onListItemTextClick(item, $event)\">\n @for (\n chunk of (listItemName(item) | syslang) ?? \"\"\n | highlightWord: searchText() : 10;\n track $index\n ) {\n <span [class]=\"{ 'font-bold': chunk.match }\" aria-hidden=\"true\">{{\n chunk.text\n }}</span>\n }\n </span>\n @if (showCount() && item.count > 0) {\n <span class=\"ml-auto px-1 text-sm empty:hidden\" aria-hidden=\"true\">{{\n item.count\n }}</span>\n }\n }\n </a>\n</ng-template>\n"
|
|
28974
29588
|
}]
|
|
28975
29589
|
}],
|
|
28976
29590
|
ctorParameters: () => [],
|
|
@@ -33205,7 +33819,7 @@ var FiltersBarComponent = class FiltersBarComponent {
|
|
|
33205
33819
|
class="shrink-0 size-9 bg-(--bg-error-base) text-(--font-neutral-white) hover:bg-(--bg-error-base-alt) focus-visible:bg-(--bg-error-base-alt)"
|
|
33206
33820
|
[title]="'filters.clearAllFilters' | transloco"
|
|
33207
33821
|
(click)="clearFilters()">
|
|
33208
|
-
<
|
|
33822
|
+
<x-mark-icon />
|
|
33209
33823
|
</button>
|
|
33210
33824
|
}
|
|
33211
33825
|
|
|
@@ -33350,8 +33964,8 @@ var FiltersBarComponent = class FiltersBarComponent {
|
|
|
33350
33964
|
},
|
|
33351
33965
|
{
|
|
33352
33966
|
kind: "component",
|
|
33353
|
-
type:
|
|
33354
|
-
selector: "
|
|
33967
|
+
type: XMarkIcon,
|
|
33968
|
+
selector: "x-mark, x-mark-icon, XMark, XMarkIcon",
|
|
33355
33969
|
inputs: ["class"]
|
|
33356
33970
|
},
|
|
33357
33971
|
{
|
|
@@ -33385,7 +33999,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
33385
33999
|
OverflowItemDirective,
|
|
33386
34000
|
OverflowStopDirective,
|
|
33387
34001
|
TranslocoPipe,
|
|
33388
|
-
|
|
34002
|
+
XMarkIcon,
|
|
33389
34003
|
InboxIcon
|
|
33390
34004
|
],
|
|
33391
34005
|
providers: [provideTranslocoScope("filters")],
|
|
@@ -33405,7 +34019,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
33405
34019
|
class="shrink-0 size-9 bg-(--bg-error-base) text-(--font-neutral-white) hover:bg-(--bg-error-base-alt) focus-visible:bg-(--bg-error-base-alt)"
|
|
33406
34020
|
[title]="'filters.clearAllFilters' | transloco"
|
|
33407
34021
|
(click)="clearFilters()">
|
|
33408
|
-
<
|
|
34022
|
+
<x-mark-icon />
|
|
33409
34023
|
</button>
|
|
33410
34024
|
}
|
|
33411
34025
|
|
|
@@ -33703,6 +34317,7 @@ var QuickFiltersListComponent = class QuickFiltersListComponent {
|
|
|
33703
34317
|
transloco = inject(TranslocoService);
|
|
33704
34318
|
host = inject(ElementRef);
|
|
33705
34319
|
injector = inject(Injector);
|
|
34320
|
+
isTouch = inject(BreakpointObserverService).isTouch;
|
|
33706
34321
|
deleteKeys = isApplePlatform() ? ["Delete", "Backspace"] : ["Delete"];
|
|
33707
34322
|
quickFilters = computed(() => this.userSettingsStore.quickFilters() ?? [], ...ngDevMode ? [{ debugName: "quickFilters" }] : /* istanbul ignore next */ []);
|
|
33708
34323
|
query = signal("", ...ngDevMode ? [{ debugName: "query" }] : /* istanbul ignore next */ []);
|
|
@@ -33715,6 +34330,7 @@ var QuickFiltersListComponent = class QuickFiltersListComponent {
|
|
|
33715
34330
|
detailsCard = viewChild("detailsCard", ...ngDevMode ? [{ debugName: "detailsCard" }] : /* istanbul ignore next */ []);
|
|
33716
34331
|
hideTimer;
|
|
33717
34332
|
deleting = signal(false, ...ngDevMode ? [{ debugName: "deleting" }] : /* istanbul ignore next */ []);
|
|
34333
|
+
restoringFocus = false;
|
|
33718
34334
|
constructor() {
|
|
33719
34335
|
injectFloatingPanel({
|
|
33720
34336
|
panel: () => this.detailsCard()?.nativeElement,
|
|
@@ -33743,6 +34359,26 @@ var QuickFiltersListComponent = class QuickFiltersListComponent {
|
|
|
33743
34359
|
anchor
|
|
33744
34360
|
});
|
|
33745
34361
|
}
|
|
34362
|
+
onRowFocus(quickFilter, anchor) {
|
|
34363
|
+
if (!this.restoringFocus && !this.isTouch()) this.showDetails(quickFilter, anchor);
|
|
34364
|
+
}
|
|
34365
|
+
onCountEnter(quickFilter, anchor) {
|
|
34366
|
+
if (!this.isTouch()) this.showDetails(quickFilter, anchor);
|
|
34367
|
+
}
|
|
34368
|
+
toggleDetails(quickFilter, anchor, event) {
|
|
34369
|
+
event.stopPropagation();
|
|
34370
|
+
if (this.details()?.quickFilter === quickFilter) {
|
|
34371
|
+
this.cancelHideDetails();
|
|
34372
|
+
this.details.set(null);
|
|
34373
|
+
} else this.showDetails(quickFilter, anchor);
|
|
34374
|
+
}
|
|
34375
|
+
onDocumentPointerDown(event) {
|
|
34376
|
+
if (!this.details()) return;
|
|
34377
|
+
const target = event.target;
|
|
34378
|
+
if (this.detailsCard()?.nativeElement.contains(target) || target?.closest("[data-details-toggle]")) return;
|
|
34379
|
+
this.cancelHideDetails();
|
|
34380
|
+
this.details.set(null);
|
|
34381
|
+
}
|
|
33746
34382
|
scheduleHideDetails() {
|
|
33747
34383
|
this.cancelHideDetails();
|
|
33748
34384
|
this.hideTimer = setTimeout(() => this.details.set(null), 100);
|
|
@@ -33769,7 +34405,7 @@ var QuickFiltersListComponent = class QuickFiltersListComponent {
|
|
|
33769
34405
|
this.applied.emit(quickFilter);
|
|
33770
34406
|
}
|
|
33771
34407
|
async remove(quickFilter, index, event) {
|
|
33772
|
-
event
|
|
34408
|
+
event?.stopPropagation();
|
|
33773
34409
|
if (this.deleting()) return;
|
|
33774
34410
|
this.deleting.set(true);
|
|
33775
34411
|
this.cancelHideDetails();
|
|
@@ -33783,7 +34419,12 @@ var QuickFiltersListComponent = class QuickFiltersListComponent {
|
|
|
33783
34419
|
this.deleted.emit(quickFilter);
|
|
33784
34420
|
if (hadFocus) afterNextRender(() => {
|
|
33785
34421
|
const rows = this.host.nativeElement.querySelectorAll("[role='option']");
|
|
33786
|
-
|
|
34422
|
+
this.restoringFocus = true;
|
|
34423
|
+
try {
|
|
34424
|
+
(rows.length ? rows[Math.min(index, rows.length - 1)] : this.host.nativeElement).focus();
|
|
34425
|
+
} finally {
|
|
34426
|
+
this.restoringFocus = false;
|
|
34427
|
+
}
|
|
33787
34428
|
}, { injector: this.injector });
|
|
33788
34429
|
} finally {
|
|
33789
34430
|
this.deleting.set(false);
|
|
@@ -33832,7 +34473,10 @@ var QuickFiltersListComponent = class QuickFiltersListComponent {
|
|
|
33832
34473
|
},
|
|
33833
34474
|
host: {
|
|
33834
34475
|
attributes: { "tabindex": "-1" },
|
|
33835
|
-
listeners: {
|
|
34476
|
+
listeners: {
|
|
34477
|
+
"keydown.escape": "onEscape($event)",
|
|
34478
|
+
"document:pointerdown": "onDocumentPointerDown($event)"
|
|
34479
|
+
},
|
|
33836
34480
|
classAttribute: "flex min-h-0 flex-col gap-(--spacing-xs) outline-none"
|
|
33837
34481
|
},
|
|
33838
34482
|
providers: [provideTranslocoScope("quick-filters")],
|
|
@@ -33867,10 +34511,17 @@ var QuickFiltersListComponent = class QuickFiltersListComponent {
|
|
|
33867
34511
|
[value]="picked()"
|
|
33868
34512
|
(valueChange)="pick($event)">
|
|
33869
34513
|
@for (quickFilter of filtered(); track quickFilter.id; let index = $index) {
|
|
34514
|
+
<!-- Touch: drag the row to the left to delete it, which replaces the cross (hidden there, see the row's class). -->
|
|
34515
|
+
<SwipeAction endEnabled [disabled]="!deletable()" endTone="error" (swipedEnd)="remove(quickFilter, index)">
|
|
34516
|
+
<span swipeActionEnd class="flex items-center gap-2">
|
|
34517
|
+
{{ "quickFilters.swipeDelete" | transloco }}
|
|
34518
|
+
<x-mark-icon class="size-4" />
|
|
34519
|
+
</span>
|
|
33870
34520
|
<ListItem
|
|
33871
34521
|
[value]="quickFilter"
|
|
33872
34522
|
[label]="quickFilter.name"
|
|
33873
|
-
|
|
34523
|
+
class="pointer-coarse:[&_.list-item-icon-right]:hidden!"
|
|
34524
|
+
(focusin)="onRowFocus(quickFilter, count)"
|
|
33874
34525
|
(focusout)="scheduleHideDetails()">
|
|
33875
34526
|
<ListItemLeft>
|
|
33876
34527
|
<ListItemIcon><quick-filter-icon /></ListItemIcon>
|
|
@@ -33883,11 +34534,20 @@ var QuickFiltersListComponent = class QuickFiltersListComponent {
|
|
|
33883
34534
|
#count
|
|
33884
34535
|
data-count
|
|
33885
34536
|
class="list-item-subtext block w-fit! decoration-dotted underline-offset-2 hover:underline"
|
|
33886
|
-
(mouseenter)="
|
|
34537
|
+
(mouseenter)="onCountEnter(quickFilter, count)"
|
|
33887
34538
|
(mouseleave)="scheduleHideDetails()">
|
|
33888
34539
|
{{ (quickFilter.filters.length === 1 ? "quickFilters.countOne" : "quickFilters.countOther") | transloco: { count: quickFilter.filters.length } }}
|
|
33889
34540
|
</span>
|
|
33890
34541
|
</ListItemText>
|
|
34542
|
+
<!-- Touch only: there is no hover to show what the quick filter holds, and a tap on the row applies it. This opens the card
|
|
34543
|
+
instead. Not a button (a listbox option holds no control, see ListItemAction): the keyboard has the focus of the row. -->
|
|
34544
|
+
<span
|
|
34545
|
+
data-details-toggle
|
|
34546
|
+
aria-hidden="true"
|
|
34547
|
+
class="hidden size-8 shrink-0 cursor-pointer items-center justify-center rounded-(--radius-xs) text-(--font-neutral-muted) pointer-coarse:inline-flex active:bg-(--bg-neutral-base)"
|
|
34548
|
+
(click)="toggleDetails(quickFilter, count, $event)">
|
|
34549
|
+
<info-circle-icon class="size-5" />
|
|
34550
|
+
</span>
|
|
33891
34551
|
</ListItemLeft>
|
|
33892
34552
|
@if (deletable()) {
|
|
33893
34553
|
<ListItemAction
|
|
@@ -33900,6 +34560,7 @@ var QuickFiltersListComponent = class QuickFiltersListComponent {
|
|
|
33900
34560
|
</ListItemAction>
|
|
33901
34561
|
}
|
|
33902
34562
|
</ListItem>
|
|
34563
|
+
</SwipeAction>
|
|
33903
34564
|
}
|
|
33904
34565
|
</List>
|
|
33905
34566
|
|
|
@@ -33949,6 +34610,12 @@ var QuickFiltersListComponent = class QuickFiltersListComponent {
|
|
|
33949
34610
|
selector: "fa-icon, FaIcon",
|
|
33950
34611
|
inputs: ["faClass", "class"]
|
|
33951
34612
|
},
|
|
34613
|
+
{
|
|
34614
|
+
kind: "component",
|
|
34615
|
+
type: InfoCircleIcon,
|
|
34616
|
+
selector: "info-circle, info-circle-icon, InfoCircle, InfoCircleIcon",
|
|
34617
|
+
inputs: ["class"]
|
|
34618
|
+
},
|
|
33952
34619
|
{
|
|
33953
34620
|
kind: "directive",
|
|
33954
34621
|
type: ListComponent,
|
|
@@ -34015,6 +34682,22 @@ var QuickFiltersListComponent = class QuickFiltersListComponent {
|
|
|
34015
34682
|
type: SearchInputComponent,
|
|
34016
34683
|
selector: "SearchInput:not([suggestions]), search-input:not([suggestions]), searchinput:not([suggestions])"
|
|
34017
34684
|
},
|
|
34685
|
+
{
|
|
34686
|
+
kind: "component",
|
|
34687
|
+
type: SwipeActionComponent,
|
|
34688
|
+
selector: "swipe-action, SwipeAction",
|
|
34689
|
+
inputs: [
|
|
34690
|
+
"class",
|
|
34691
|
+
"contentClass",
|
|
34692
|
+
"startEnabled",
|
|
34693
|
+
"endEnabled",
|
|
34694
|
+
"startTone",
|
|
34695
|
+
"endTone",
|
|
34696
|
+
"disabled",
|
|
34697
|
+
"threshold"
|
|
34698
|
+
],
|
|
34699
|
+
outputs: ["swipedStart", "swipedEnd"]
|
|
34700
|
+
},
|
|
34018
34701
|
{
|
|
34019
34702
|
kind: "directive",
|
|
34020
34703
|
type: TooltipDirective,
|
|
@@ -34074,6 +34757,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
34074
34757
|
imports: [
|
|
34075
34758
|
TranslocoPipe,
|
|
34076
34759
|
FaIconComponent,
|
|
34760
|
+
InfoCircleIcon,
|
|
34077
34761
|
ListComponent,
|
|
34078
34762
|
ListItemComponent,
|
|
34079
34763
|
ListItemActionComponent,
|
|
@@ -34082,6 +34766,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
34082
34766
|
ListItemTextComponent,
|
|
34083
34767
|
QuickFilterIcon,
|
|
34084
34768
|
SearchInputComponent,
|
|
34769
|
+
SwipeActionComponent,
|
|
34085
34770
|
TooltipDirective,
|
|
34086
34771
|
TruncatedDirective,
|
|
34087
34772
|
XMarkIcon
|
|
@@ -34090,7 +34775,8 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
34090
34775
|
host: {
|
|
34091
34776
|
class: "flex min-h-0 flex-col gap-(--spacing-xs) outline-none",
|
|
34092
34777
|
tabindex: "-1",
|
|
34093
|
-
"(keydown.escape)": "onEscape($event)"
|
|
34778
|
+
"(keydown.escape)": "onEscape($event)",
|
|
34779
|
+
"(document:pointerdown)": "onDocumentPointerDown($event)"
|
|
34094
34780
|
},
|
|
34095
34781
|
template: `
|
|
34096
34782
|
@if (searchable() && quickFilters().length > 0) {
|
|
@@ -34115,10 +34801,17 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
34115
34801
|
[value]="picked()"
|
|
34116
34802
|
(valueChange)="pick($event)">
|
|
34117
34803
|
@for (quickFilter of filtered(); track quickFilter.id; let index = $index) {
|
|
34804
|
+
<!-- Touch: drag the row to the left to delete it, which replaces the cross (hidden there, see the row's class). -->
|
|
34805
|
+
<SwipeAction endEnabled [disabled]="!deletable()" endTone="error" (swipedEnd)="remove(quickFilter, index)">
|
|
34806
|
+
<span swipeActionEnd class="flex items-center gap-2">
|
|
34807
|
+
{{ "quickFilters.swipeDelete" | transloco }}
|
|
34808
|
+
<x-mark-icon class="size-4" />
|
|
34809
|
+
</span>
|
|
34118
34810
|
<ListItem
|
|
34119
34811
|
[value]="quickFilter"
|
|
34120
34812
|
[label]="quickFilter.name"
|
|
34121
|
-
|
|
34813
|
+
class="pointer-coarse:[&_.list-item-icon-right]:hidden!"
|
|
34814
|
+
(focusin)="onRowFocus(quickFilter, count)"
|
|
34122
34815
|
(focusout)="scheduleHideDetails()">
|
|
34123
34816
|
<ListItemLeft>
|
|
34124
34817
|
<ListItemIcon><quick-filter-icon /></ListItemIcon>
|
|
@@ -34131,11 +34824,20 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
34131
34824
|
#count
|
|
34132
34825
|
data-count
|
|
34133
34826
|
class="list-item-subtext block w-fit! decoration-dotted underline-offset-2 hover:underline"
|
|
34134
|
-
(mouseenter)="
|
|
34827
|
+
(mouseenter)="onCountEnter(quickFilter, count)"
|
|
34135
34828
|
(mouseleave)="scheduleHideDetails()">
|
|
34136
34829
|
{{ (quickFilter.filters.length === 1 ? "quickFilters.countOne" : "quickFilters.countOther") | transloco: { count: quickFilter.filters.length } }}
|
|
34137
34830
|
</span>
|
|
34138
34831
|
</ListItemText>
|
|
34832
|
+
<!-- Touch only: there is no hover to show what the quick filter holds, and a tap on the row applies it. This opens the card
|
|
34833
|
+
instead. Not a button (a listbox option holds no control, see ListItemAction): the keyboard has the focus of the row. -->
|
|
34834
|
+
<span
|
|
34835
|
+
data-details-toggle
|
|
34836
|
+
aria-hidden="true"
|
|
34837
|
+
class="hidden size-8 shrink-0 cursor-pointer items-center justify-center rounded-(--radius-xs) text-(--font-neutral-muted) pointer-coarse:inline-flex active:bg-(--bg-neutral-base)"
|
|
34838
|
+
(click)="toggleDetails(quickFilter, count, $event)">
|
|
34839
|
+
<info-circle-icon class="size-5" />
|
|
34840
|
+
</span>
|
|
34139
34841
|
</ListItemLeft>
|
|
34140
34842
|
@if (deletable()) {
|
|
34141
34843
|
<ListItemAction
|
|
@@ -34148,6 +34850,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
34148
34850
|
</ListItemAction>
|
|
34149
34851
|
}
|
|
34150
34852
|
</ListItem>
|
|
34853
|
+
</SwipeAction>
|
|
34151
34854
|
}
|
|
34152
34855
|
</List>
|
|
34153
34856
|
|
|
@@ -34969,6 +35672,8 @@ var MultiSelectLabelsComponent = class MultiSelectLabelsComponent {
|
|
|
34969
35672
|
"fieldLabelLang",
|
|
34970
35673
|
"placeholder",
|
|
34971
35674
|
"ariaLabel",
|
|
35675
|
+
"hideKeyboardLabel",
|
|
35676
|
+
"keyboardLabel",
|
|
34972
35677
|
"noResultsText",
|
|
34973
35678
|
"searchingText",
|
|
34974
35679
|
"errorText",
|
|
@@ -35411,7 +36116,8 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
35411
36116
|
}]
|
|
35412
36117
|
});
|
|
35413
36118
|
const ROW_BUTTON = "flex size-5 shrink-0 cursor-pointer items-center justify-center overflow-hidden rounded-full border-0 bg-transparent p-0 transition-[max-width,opacity,background-color,color] duration-150 motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-solid focus-visible:outline-(--stroke-focus) focus-visible:-outline-offset-2";
|
|
35414
|
-
const ROW_BUTTON_HIDDEN = "max-w-0 opacity-0 group-hover/row:max-w-5 group-hover/row:opacity-100 group-focus-within/row:max-w-5 group-focus-within/row:opacity-100";
|
|
36119
|
+
const ROW_BUTTON_HIDDEN = "max-w-0 opacity-0 group-hover/row:max-w-5 group-hover/row:opacity-100 group-focus-within/row:max-w-5 group-focus-within/row:opacity-100 pointer-coarse:sr-only";
|
|
36120
|
+
const INFO_BUTTON = "hidden size-8 shrink-0 cursor-pointer items-center justify-center rounded-full border-0 bg-transparent p-0 text-(--history-panel-icon,var(--font-neutral-muted)) active:bg-(--history-panel-active,var(--bg-neutral-hovered)) focus-visible:outline-2 focus-visible:outline-solid focus-visible:outline-(--stroke-focus) focus-visible:-outline-offset-2 pointer-coarse:flex";
|
|
35415
36121
|
const SHEET_CLASSES = [
|
|
35416
36122
|
"bg-(--history-panel-bg,var(--layer-page-layer)) text-(--history-panel-fg,var(--layer-page-font))",
|
|
35417
36123
|
"[&_.modal-title]:text-(--history-panel-fg,var(--layer-page-font))",
|
|
@@ -35432,6 +36138,7 @@ var HistoryPanelComponent = class HistoryPanelComponent {
|
|
|
35432
36138
|
sheetClasses = SHEET_CLASSES;
|
|
35433
36139
|
rowButton = ROW_BUTTON;
|
|
35434
36140
|
rowButtonHidden = ROW_BUTTON_HIDDEN;
|
|
36141
|
+
infoButton = INFO_BUTTON;
|
|
35435
36142
|
asideClasses = computed(() => cn("flex min-h-0 shrink-0 flex-col overflow-hidden whitespace-normal rounded-(--radius-lg) bg-(--history-panel-bg,var(--layer-page-layer)) text-(--history-panel-fg,var(--layer-page-font))", "transition-[width,padding,margin] duration-350 ease-[cubic-bezier(0.32,0.72,0,1)] motion-reduce:transition-none", this.open() ? "w-(--history-panel-width,15.625rem) p-(--spacing-xs) ms-(--history-panel-gap,0px)" : "w-0 p-0 ms-0", this.class()), ...ngDevMode ? [{ debugName: "asideClasses" }] : /* istanbul ignore next */ []);
|
|
35436
36143
|
searches = computed(() => {
|
|
35437
36144
|
const recent = this.userSettingsStore.recentSearches() ?? [];
|
|
@@ -35452,6 +36159,10 @@ var HistoryPanelComponent = class HistoryPanelComponent {
|
|
|
35452
36159
|
onTab(value) {
|
|
35453
36160
|
if (value[0] === "all" || value[0] === "favorites") this.tab.set(value[0]);
|
|
35454
36161
|
}
|
|
36162
|
+
toggleTitle(tip) {
|
|
36163
|
+
if (tip.isOpen()) tip.hide();
|
|
36164
|
+
else tip.show();
|
|
36165
|
+
}
|
|
35455
36166
|
onEscape(event) {
|
|
35456
36167
|
if (!this.open()) return;
|
|
35457
36168
|
event.stopPropagation();
|
|
@@ -35572,9 +36283,26 @@ var HistoryPanelComponent = class HistoryPanelComponent {
|
|
|
35572
36283
|
</h3>
|
|
35573
36284
|
<ul class="m-0 flex list-none flex-col gap-(--spacing-3xs) p-0">
|
|
35574
36285
|
@for (search of group.items; track search.url) {
|
|
35575
|
-
<li
|
|
35576
|
-
|
|
35577
|
-
|
|
36286
|
+
<li historyTitleMarquee class="group/row min-w-0 rounded-(--radius-sm)">
|
|
36287
|
+
<!-- Touch: drag the row to the left to delete it, to the right to (un)favorite it. The buttons below are
|
|
36288
|
+
then out of sight but still there for a screen reader or a switch (see ROW_BUTTON_HIDDEN). -->
|
|
36289
|
+
<SwipeAction
|
|
36290
|
+
class="rounded-(--radius-sm)"
|
|
36291
|
+
contentClass="flex min-h-8 min-w-0 items-center gap-(--spacing-3xs) rounded-(--radius-sm) hover:bg-(--history-panel-hover,var(--bg-neutral-base)) focus-within:bg-(--history-panel-hover,var(--bg-neutral-base)) active:bg-(--history-panel-active,var(--bg-neutral-hovered))"
|
|
36292
|
+
startEnabled
|
|
36293
|
+
endEnabled
|
|
36294
|
+
startTone="warning"
|
|
36295
|
+
endTone="error"
|
|
36296
|
+
(swipedStart)="toggleFavorite(search)"
|
|
36297
|
+
(swipedEnd)="remove(search)">
|
|
36298
|
+
<span swipeActionStart class="flex items-center gap-2">
|
|
36299
|
+
<star-icon class="size-4" [solid]="!isFavorite(search)" />
|
|
36300
|
+
{{ (isFavorite(search) ? 'searches.history.removeFavorite' : 'searches.history.addFavorite') | transloco }}
|
|
36301
|
+
</span>
|
|
36302
|
+
<span swipeActionEnd class="flex items-center gap-2">
|
|
36303
|
+
{{ 'searches.recent.remove' | transloco }}
|
|
36304
|
+
<x-mark-icon class="size-4" />
|
|
36305
|
+
</span>
|
|
35578
36306
|
<button
|
|
35579
36307
|
type="button"
|
|
35580
36308
|
class="flex min-w-0 flex-1 cursor-pointer items-center gap-(--spacing-xs) rounded-(--radius-sm) border-0 bg-transparent py-1.5 ps-(--spacing-2xs) pe-(--spacing-3xs) text-left text-inherit focus-visible:outline-2 focus-visible:outline-solid focus-visible:outline-(--stroke-focus) focus-visible:-outline-offset-2"
|
|
@@ -35586,12 +36314,22 @@ var HistoryPanelComponent = class HistoryPanelComponent {
|
|
|
35586
36314
|
data-title-box
|
|
35587
36315
|
truncated
|
|
35588
36316
|
#cut="truncated"
|
|
36317
|
+
#tip="tooltip"
|
|
35589
36318
|
[tooltip]="titleOf(search)"
|
|
35590
36319
|
[tooltipDisabled]="!cut.isTruncated()"
|
|
35591
36320
|
class="block min-w-0 flex-1 overflow-hidden text-sm leading-[18px] whitespace-nowrap text-(--history-panel-fg,var(--layer-page-font)) [mask-image:linear-gradient(to_right,black_85%,transparent_100%)]">
|
|
35592
36321
|
<span class="inline-block whitespace-nowrap">{{ titleOf(search) }}</span>
|
|
35593
36322
|
</span>
|
|
35594
36323
|
</button>
|
|
36324
|
+
@if (cut.isTruncated()) {
|
|
36325
|
+
<button
|
|
36326
|
+
type="button"
|
|
36327
|
+
[class]="infoButton"
|
|
36328
|
+
[attr.aria-label]="'searches.history.showFullTitle' | transloco"
|
|
36329
|
+
(click)="toggleTitle(tip)">
|
|
36330
|
+
<info-circle-icon class="size-4" />
|
|
36331
|
+
</button>
|
|
36332
|
+
}
|
|
35595
36333
|
<span class="flex shrink-0 items-center gap-0.5 pe-(--spacing-2xs)">
|
|
35596
36334
|
<button
|
|
35597
36335
|
type="button"
|
|
@@ -35609,6 +36347,7 @@ var HistoryPanelComponent = class HistoryPanelComponent {
|
|
|
35609
36347
|
<star-icon class="size-3.5" [solid]="isFavorite(search)" />
|
|
35610
36348
|
</button>
|
|
35611
36349
|
</span>
|
|
36350
|
+
</SwipeAction>
|
|
35612
36351
|
</li>
|
|
35613
36352
|
}
|
|
35614
36353
|
</ul>
|
|
@@ -35685,6 +36424,12 @@ var HistoryPanelComponent = class HistoryPanelComponent {
|
|
|
35685
36424
|
type: HistoryTitleMarqueeDirective,
|
|
35686
36425
|
selector: "[historyTitleMarquee]"
|
|
35687
36426
|
},
|
|
36427
|
+
{
|
|
36428
|
+
kind: "component",
|
|
36429
|
+
type: InfoCircleIcon,
|
|
36430
|
+
selector: "info-circle, info-circle-icon, InfoCircle, InfoCircleIcon",
|
|
36431
|
+
inputs: ["class"]
|
|
36432
|
+
},
|
|
35688
36433
|
{
|
|
35689
36434
|
kind: "component",
|
|
35690
36435
|
type: ListPanelEmptyComponent,
|
|
@@ -35732,6 +36477,22 @@ var HistoryPanelComponent = class HistoryPanelComponent {
|
|
|
35732
36477
|
selector: "star, star-icon, Star, StarIcon",
|
|
35733
36478
|
inputs: ["class", "solid"]
|
|
35734
36479
|
},
|
|
36480
|
+
{
|
|
36481
|
+
kind: "component",
|
|
36482
|
+
type: SwipeActionComponent,
|
|
36483
|
+
selector: "swipe-action, SwipeAction",
|
|
36484
|
+
inputs: [
|
|
36485
|
+
"class",
|
|
36486
|
+
"contentClass",
|
|
36487
|
+
"startEnabled",
|
|
36488
|
+
"endEnabled",
|
|
36489
|
+
"startTone",
|
|
36490
|
+
"endTone",
|
|
36491
|
+
"disabled",
|
|
36492
|
+
"threshold"
|
|
36493
|
+
],
|
|
36494
|
+
outputs: ["swipedStart", "swipedEnd"]
|
|
36495
|
+
},
|
|
35735
36496
|
{
|
|
35736
36497
|
kind: "directive",
|
|
35737
36498
|
type: ToggleGroupComponent,
|
|
@@ -35821,12 +36582,14 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
35821
36582
|
DialogTitleComponent,
|
|
35822
36583
|
HistoryIcon,
|
|
35823
36584
|
HistoryTitleMarqueeDirective,
|
|
36585
|
+
InfoCircleIcon,
|
|
35824
36586
|
ListPanelEmptyComponent,
|
|
35825
36587
|
MagnifyingGlassIcon,
|
|
35826
36588
|
SheetBodyComponent,
|
|
35827
36589
|
SheetComponent,
|
|
35828
36590
|
SheetHeaderComponent,
|
|
35829
36591
|
StarIcon,
|
|
36592
|
+
SwipeActionComponent,
|
|
35830
36593
|
ToggleGroupComponent,
|
|
35831
36594
|
ToggleItemComponent,
|
|
35832
36595
|
TooltipDirective,
|
|
@@ -35879,9 +36642,26 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
35879
36642
|
</h3>
|
|
35880
36643
|
<ul class="m-0 flex list-none flex-col gap-(--spacing-3xs) p-0">
|
|
35881
36644
|
@for (search of group.items; track search.url) {
|
|
35882
|
-
<li
|
|
35883
|
-
|
|
35884
|
-
|
|
36645
|
+
<li historyTitleMarquee class="group/row min-w-0 rounded-(--radius-sm)">
|
|
36646
|
+
<!-- Touch: drag the row to the left to delete it, to the right to (un)favorite it. The buttons below are
|
|
36647
|
+
then out of sight but still there for a screen reader or a switch (see ROW_BUTTON_HIDDEN). -->
|
|
36648
|
+
<SwipeAction
|
|
36649
|
+
class="rounded-(--radius-sm)"
|
|
36650
|
+
contentClass="flex min-h-8 min-w-0 items-center gap-(--spacing-3xs) rounded-(--radius-sm) hover:bg-(--history-panel-hover,var(--bg-neutral-base)) focus-within:bg-(--history-panel-hover,var(--bg-neutral-base)) active:bg-(--history-panel-active,var(--bg-neutral-hovered))"
|
|
36651
|
+
startEnabled
|
|
36652
|
+
endEnabled
|
|
36653
|
+
startTone="warning"
|
|
36654
|
+
endTone="error"
|
|
36655
|
+
(swipedStart)="toggleFavorite(search)"
|
|
36656
|
+
(swipedEnd)="remove(search)">
|
|
36657
|
+
<span swipeActionStart class="flex items-center gap-2">
|
|
36658
|
+
<star-icon class="size-4" [solid]="!isFavorite(search)" />
|
|
36659
|
+
{{ (isFavorite(search) ? 'searches.history.removeFavorite' : 'searches.history.addFavorite') | transloco }}
|
|
36660
|
+
</span>
|
|
36661
|
+
<span swipeActionEnd class="flex items-center gap-2">
|
|
36662
|
+
{{ 'searches.recent.remove' | transloco }}
|
|
36663
|
+
<x-mark-icon class="size-4" />
|
|
36664
|
+
</span>
|
|
35885
36665
|
<button
|
|
35886
36666
|
type="button"
|
|
35887
36667
|
class="flex min-w-0 flex-1 cursor-pointer items-center gap-(--spacing-xs) rounded-(--radius-sm) border-0 bg-transparent py-1.5 ps-(--spacing-2xs) pe-(--spacing-3xs) text-left text-inherit focus-visible:outline-2 focus-visible:outline-solid focus-visible:outline-(--stroke-focus) focus-visible:-outline-offset-2"
|
|
@@ -35893,12 +36673,22 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
35893
36673
|
data-title-box
|
|
35894
36674
|
truncated
|
|
35895
36675
|
#cut="truncated"
|
|
36676
|
+
#tip="tooltip"
|
|
35896
36677
|
[tooltip]="titleOf(search)"
|
|
35897
36678
|
[tooltipDisabled]="!cut.isTruncated()"
|
|
35898
36679
|
class="block min-w-0 flex-1 overflow-hidden text-sm leading-[18px] whitespace-nowrap text-(--history-panel-fg,var(--layer-page-font)) [mask-image:linear-gradient(to_right,black_85%,transparent_100%)]">
|
|
35899
36680
|
<span class="inline-block whitespace-nowrap">{{ titleOf(search) }}</span>
|
|
35900
36681
|
</span>
|
|
35901
36682
|
</button>
|
|
36683
|
+
@if (cut.isTruncated()) {
|
|
36684
|
+
<button
|
|
36685
|
+
type="button"
|
|
36686
|
+
[class]="infoButton"
|
|
36687
|
+
[attr.aria-label]="'searches.history.showFullTitle' | transloco"
|
|
36688
|
+
(click)="toggleTitle(tip)">
|
|
36689
|
+
<info-circle-icon class="size-4" />
|
|
36690
|
+
</button>
|
|
36691
|
+
}
|
|
35902
36692
|
<span class="flex shrink-0 items-center gap-0.5 pe-(--spacing-2xs)">
|
|
35903
36693
|
<button
|
|
35904
36694
|
type="button"
|
|
@@ -35916,6 +36706,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
35916
36706
|
<star-icon class="size-3.5" [solid]="isFavorite(search)" />
|
|
35917
36707
|
</button>
|
|
35918
36708
|
</span>
|
|
36709
|
+
</SwipeAction>
|
|
35919
36710
|
</li>
|
|
35920
36711
|
}
|
|
35921
36712
|
</ul>
|
|
@@ -38772,6 +39563,10 @@ const userProfileSchema = (editing) => schema((p) => {
|
|
|
38772
39563
|
applyEach(p.customData, (field) => readonly(field, { when: ({ key }) => `customData.${key()}` !== editing() }));
|
|
38773
39564
|
});
|
|
38774
39565
|
var UserProfileFormComponent = class UserProfileFormComponent {
|
|
39566
|
+
padded = input(true, {
|
|
39567
|
+
...ngDevMode ? { debugName: "padded" } : /* istanbul ignore next */ {},
|
|
39568
|
+
transform: booleanAttribute
|
|
39569
|
+
});
|
|
38775
39570
|
principalStore = inject(PrincipalStore);
|
|
38776
39571
|
userProfileService = inject(UserProfileService);
|
|
38777
39572
|
appStore = inject(AppStore);
|
|
@@ -39116,10 +39911,17 @@ var UserProfileFormComponent = class UserProfileFormComponent {
|
|
|
39116
39911
|
type: UserProfileFormComponent,
|
|
39117
39912
|
isStandalone: true,
|
|
39118
39913
|
selector: "user-profile-form, UserProfileForm, userprofileform",
|
|
39914
|
+
inputs: { padded: {
|
|
39915
|
+
classPropertyName: "padded",
|
|
39916
|
+
publicName: "padded",
|
|
39917
|
+
isSignal: true,
|
|
39918
|
+
isRequired: false,
|
|
39919
|
+
transformFunction: null
|
|
39920
|
+
} },
|
|
39119
39921
|
host: { classAttribute: "flex flex-col h-full overflow-hidden" },
|
|
39120
39922
|
providers: [provideTranslocoScope("user-profile", "login")],
|
|
39121
39923
|
ngImport: i0,
|
|
39122
|
-
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 class=\"@container\"\n [formRoot]=\"profileForm\"\n [attr.aria-label]=\"'userProfile.profileFields' | transloco\"\n [attr.aria-busy]=\"profileForm().submitting() ? 'true' : null\">\n @for (section of sections(); track section.id) {\n <section\n [attr.aria-labelledby]=\"'user-profile-section-' + section.id\"\n class=\"mb-(--spacing-md) flex flex-col gap-(--spacing-sm) rounded-(--radius-lg) border border-solid border-(--stroke-primary-lighter) p-(--spacing-md)\">\n <div>\n <h3\n [id]=\"'user-profile-section-' + section.id\"\n class=\"m-0 text-(length:--size-base) font-semibold\">\n {{ \"userProfile.sections.\" + section.id | transloco }}\n </h3>\n <p\n class=\"m-0 text-(length:--size-sm) text-(--font-neutral-muted)\">\n {{\n \"userProfile.sections.\" + section.id + \"Hint\" | transloco\n }}\n </p>\n </div>\n <div\n class=\"grid grid-cols-1 gap-x-(--spacing-md) gap-y-(--spacing-sm) @lg:grid-cols-2\">\n @for (\n property of section.rows;\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 = labelFor(category, key);\n @let editing = isEditing(category, key);\n @let invalid = showError(field);\n @let boolean = isBoolean(category, key);\n @let readOnlyValue = isReadOnlyValue(category, key);\n @let leaf = isLeaf(category, key);\n @let saved = isSaved(category, key);\n <!-- role=\"group\" named by the label: without it the 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. A switch, and a value shown but not edited,\n take the whole width of the grid: they are not a field to line up with another. -->\n <div\n role=\"group\"\n [attr.aria-labelledby]=\"labelId(category, key)\"\n class=\"flex min-w-0 flex-col gap-(--spacing-3xs)\"\n [class.col-span-full]=\"boolean || readOnlyValue\">\n @if (boolean) {\n <!-- A switch is not a labelable text field: the name goes through aria-labelledby. The\n value is saved as soon as it is flipped, so there is no edit mode. -->\n <div\n class=\"flex flex-row items-center gap-(--spacing-sm) rounded-(--radius-md) border border-solid border-(--stroke-primary-lighter) px-(--spacing-md) py-(--spacing-xs)\">\n <span\n [id]=\"labelId(category, key)\"\n class=\"grow\"\n [class]=\"labelClass\"\n >{{ label }}</span\n >\n @if (saved) {\n <span\n class=\"flex items-center gap-(--spacing-3xs) text-(length:--size-sm) text-(--font-success-base)\">\n <CircleCheckIcon aria-hidden=\"true\" />\n {{ \"userProfile.saved\" | transloco }}\n </span>\n } @else if (isDefault(category, key)) {\n <span [class]=\"chipClass\">{{\n \"userProfile.defaultValue\" | transloco\n }}</span>\n }\n <switch\n #toggle\n [checked]=\"booleanValue(category, key)\"\n [disabled]=\"profileForm().submitting()\"\n [aria-labelledby]=\"labelId(category, key)\"\n (checkedChange)=\"\n onToggleBoolean(category, key, $event, toggle)\n \" />\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: { property: label }\n \"\n [disabled]=\"!hasStoredValue(category, key)\"\n (click)=\"onDeleteData(category, key)\">\n <TrashIcon />\n </button>\n </div>\n } @else if (readOnlyValue) {\n <!-- An array is shown, never edited: its shape is the administrator's. A galactik\n TagInput in read-only mode, which is what it is for; its inner text field is named\n after the property, as there is no <label for> to reach it. -->\n <span\n [id]=\"labelId(category, key)\"\n [class]=\"labelClass\"\n >{{ label }}</span\n >\n <tag-input\n class=\"[&_tag-remove]:hidden\"\n [value]=\"arrayTags(category, key)\"\n [readonly]=\"true\"\n [aria-label]=\"label\"\n [addLabel]=\"label\" />\n } @else {\n <label\n [id]=\"labelId(category, key)\"\n [for]=\"fieldId(category, key)\"\n [class]=\"labelClass\"\n >{{ label }}</label\n >\n <!-- The actions sit inside the box: galactik styles it from the control with has-[]\n selectors, which match descendants, so the focus ring and the read-only look\n keep working with the buttons next to the input. At rest the box is tinted,\n which is what tells a field from its label; it is white while edited. -->\n <!-- scheme: galactik colours the field itself for an error and for a save that just\n went through. The tint of a read-only field must not hide the success one, hence\n the data attribute and its more specific variant. -->\n <input-group\n class=\"has-[[readonly]]:bg-(--bg-primary-lighter) data-[state=success]:has-[[readonly]]:bg-(--bg-success-base-alt)\"\n [attr.data-state]=\"saved ? 'success' : null\"\n [scheme]=\"\n invalid ? 'error' : saved ? 'success' : 'default'\n \">\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 <input\n input-control\n type=\"text\"\n [id]=\"fieldId(category, key)\"\n [formField]=\"field\"\n [attr.placeholder]=\"\n 'userProfile.notDefined' | transloco\n \"\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)=\"onEscape($event, category, key)\" />\n <!-- Always visible: hiding them until hover made them unreachable on a touch screen\n and left nothing to discover. -->\n @if (!leaf) {\n <div class=\"flex shrink-0 flex-row items-center\">\n @if (editing) {\n <button\n type=\"submit\"\n variant=\"primary\"\n size=\"sm\"\n [iconOnly]=\"true\"\n class=\"cursor-pointer\"\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 size=\"sm\"\n [iconOnly]=\"true\"\n class=\"cursor-pointer\"\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 size=\"sm\"\n [iconOnly]=\"true\"\n [id]=\"editButtonId(category, key)\"\n class=\"cursor-pointer\"\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 size=\"sm\"\n [iconOnly]=\"true\"\n class=\"text-destructive cursor-pointer\"\n [title]=\"\n 'userProfile.deleteProperty' | transloco\n \"\n [attr.aria-label]=\"\n 'userProfile.deletePropertyNamed'\n | transloco: { property: label }\n \"\n [disabled]=\"!hasStoredValue(category, key)\"\n (click)=\"onDeleteData(category, key)\">\n <TrashIcon />\n </button>\n }\n </div>\n }\n </input-group>\n }\n <!-- Under the field, one message at a time: the error first, then the confirmation of a\n save, then what is worth knowing about the value. -->\n @if (invalid) {\n <p\n [id]=\"errorId(category, key)\"\n role=\"alert\"\n [class]=\"errorClass\">\n {{ errorKey(field) | transloco }}\n </p>\n } @else if (!boolean) {\n @if (saved) {\n <p\n class=\"m-0 flex items-center gap-(--spacing-3xs) text-(length:--size-sm) text-(--font-success-base)\">\n <CircleCheckIcon aria-hidden=\"true\" />\n {{ \"userProfile.saved\" | transloco }}\n </p>\n } @else if (readOnlyValue || leaf) {\n <p\n class=\"m-0 flex items-center gap-(--spacing-3xs) text-(length:--size-sm) text-(--font-neutral-muted)\">\n <LockIcon />\n {{ \"userProfile.lockedByAdmin\" | transloco }}\n </p>\n } @else if (editing) {\n <p\n class=\"m-0 text-(length:--size-sm) text-(--font-neutral-muted)\">\n {{ \"userProfile.editHint\" | transloco }}\n </p>\n } @else if (isDefault(category, key)) {\n <p class=\"m-0\">\n <span [class]=\"chipClass\">{{\n \"userProfile.defaultValue\" | transloco\n }}</span>\n </p>\n }\n }\n </div>\n }\n </div>\n </section>\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",
|
|
39924
|
+
template: "@if (userProfileResource.hasValue()) {\n @let profile = user();\n <div class=\"flex min-h-0 flex-1 flex-col gap-2\" [class.p-4]=\"padded()\">\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 <XMarkIcon />\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 class=\"@container\"\n [formRoot]=\"profileForm\"\n [attr.aria-label]=\"'userProfile.profileFields' | transloco\"\n [attr.aria-busy]=\"profileForm().submitting() ? 'true' : null\">\n @for (section of sections(); track section.id) {\n <section\n [attr.aria-labelledby]=\"'user-profile-section-' + section.id\"\n class=\"mb-(--spacing-md) flex flex-col gap-(--spacing-sm) rounded-(--radius-lg) border border-solid border-(--stroke-primary-lighter) p-(--spacing-md)\">\n <div>\n <h3\n [id]=\"'user-profile-section-' + section.id\"\n class=\"m-0 text-(length:--size-base) font-semibold\">\n {{ \"userProfile.sections.\" + section.id | transloco }}\n </h3>\n <p\n class=\"m-0 text-(length:--size-sm) text-(--font-neutral-muted)\">\n {{\n \"userProfile.sections.\" + section.id + \"Hint\" | transloco\n }}\n </p>\n </div>\n <div\n class=\"grid grid-cols-1 gap-x-(--spacing-md) gap-y-(--spacing-sm) @lg:grid-cols-2\">\n @for (\n property of section.rows;\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 = labelFor(category, key);\n @let editing = isEditing(category, key);\n @let invalid = showError(field);\n @let boolean = isBoolean(category, key);\n @let readOnlyValue = isReadOnlyValue(category, key);\n @let leaf = isLeaf(category, key);\n @let saved = isSaved(category, key);\n <!-- role=\"group\" named by the label: without it the 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. A switch, and a value shown but not edited,\n take the whole width of the grid: they are not a field to line up with another. -->\n <div\n role=\"group\"\n [attr.aria-labelledby]=\"labelId(category, key)\"\n class=\"flex min-w-0 flex-col gap-(--spacing-3xs)\"\n [class.col-span-full]=\"boolean || readOnlyValue\">\n @if (boolean) {\n <!-- A switch is not a labelable text field: the name goes through aria-labelledby. The\n value is saved as soon as it is flipped, so there is no edit mode. -->\n <div\n class=\"flex flex-row items-center gap-(--spacing-sm) rounded-(--radius-md) border border-solid border-(--stroke-primary-lighter) px-(--spacing-md) py-(--spacing-xs)\">\n <span\n [id]=\"labelId(category, key)\"\n class=\"grow\"\n [class]=\"labelClass\"\n >{{ label }}</span\n >\n @if (saved) {\n <span\n class=\"flex items-center gap-(--spacing-3xs) text-(length:--size-sm) text-(--font-success-base)\">\n <CircleCheckIcon aria-hidden=\"true\" />\n {{ \"userProfile.saved\" | transloco }}\n </span>\n } @else if (isDefault(category, key)) {\n <span [class]=\"chipClass\">{{\n \"userProfile.defaultValue\" | transloco\n }}</span>\n }\n <switch\n #toggle\n [checked]=\"booleanValue(category, key)\"\n [disabled]=\"profileForm().submitting()\"\n [aria-labelledby]=\"labelId(category, key)\"\n (checkedChange)=\"\n onToggleBoolean(category, key, $event, toggle)\n \" />\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: { property: label }\n \"\n [disabled]=\"!hasStoredValue(category, key)\"\n (click)=\"onDeleteData(category, key)\">\n <XMarkIcon />\n </button>\n </div>\n } @else if (readOnlyValue) {\n <!-- An array is shown, never edited: its shape is the administrator's. A galactik\n TagInput in read-only mode, which is what it is for; its inner text field is named\n after the property, as there is no <label for> to reach it. -->\n <span\n [id]=\"labelId(category, key)\"\n [class]=\"labelClass\"\n >{{ label }}</span\n >\n <tag-input\n class=\"[&_tag-remove]:hidden\"\n [value]=\"arrayTags(category, key)\"\n [readonly]=\"true\"\n [aria-label]=\"label\"\n [addLabel]=\"label\" />\n } @else {\n <label\n [id]=\"labelId(category, key)\"\n [for]=\"fieldId(category, key)\"\n [class]=\"labelClass\"\n >{{ label }}</label\n >\n <!-- The actions sit inside the box: galactik styles it from the control with has-[]\n selectors, which match descendants, so the focus ring and the read-only look\n keep working with the buttons next to the input. At rest the box is tinted,\n which is what tells a field from its label; it is white while edited. -->\n <!-- scheme: galactik colours the field itself for an error and for a save that just\n went through. The tint of a read-only field must not hide the success one, hence\n the data attribute and its more specific variant. -->\n <input-group\n class=\"has-[[readonly]]:bg-(--bg-primary-lighter) data-[state=success]:has-[[readonly]]:bg-(--bg-success-base-alt)\"\n [attr.data-state]=\"saved ? 'success' : null\"\n [scheme]=\"\n invalid ? 'error' : saved ? 'success' : 'default'\n \">\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 <input\n input-control\n type=\"text\"\n [id]=\"fieldId(category, key)\"\n [formField]=\"field\"\n [attr.placeholder]=\"\n 'userProfile.notDefined' | transloco\n \"\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)=\"onEscape($event, category, key)\" />\n <!-- Always visible: hiding them until hover made them unreachable on a touch screen\n and left nothing to discover. -->\n @if (!leaf) {\n <div class=\"flex shrink-0 flex-row items-center\">\n @if (editing) {\n <button\n type=\"submit\"\n variant=\"primary\"\n size=\"sm\"\n [iconOnly]=\"true\"\n class=\"cursor-pointer\"\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 size=\"sm\"\n [iconOnly]=\"true\"\n class=\"cursor-pointer\"\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 size=\"sm\"\n [iconOnly]=\"true\"\n [id]=\"editButtonId(category, key)\"\n class=\"cursor-pointer\"\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 size=\"sm\"\n [iconOnly]=\"true\"\n class=\"text-destructive cursor-pointer\"\n [title]=\"\n 'userProfile.deleteProperty' | transloco\n \"\n [attr.aria-label]=\"\n 'userProfile.deletePropertyNamed'\n | transloco: { property: label }\n \"\n [disabled]=\"!hasStoredValue(category, key)\"\n (click)=\"onDeleteData(category, key)\">\n <XMarkIcon />\n </button>\n }\n </div>\n }\n </input-group>\n }\n <!-- Under the field, one message at a time: the error first, then the confirmation of a\n save, then what is worth knowing about the value. -->\n @if (invalid) {\n <p\n [id]=\"errorId(category, key)\"\n role=\"alert\"\n [class]=\"errorClass\">\n {{ errorKey(field) | transloco }}\n </p>\n } @else if (!boolean) {\n @if (saved) {\n <p\n class=\"m-0 flex items-center gap-(--spacing-3xs) text-(length:--size-sm) text-(--font-success-base)\">\n <CircleCheckIcon aria-hidden=\"true\" />\n {{ \"userProfile.saved\" | transloco }}\n </p>\n } @else if (readOnlyValue || leaf) {\n <p\n class=\"m-0 flex items-center gap-(--spacing-3xs) text-(length:--size-sm) text-(--font-neutral-muted)\">\n <LockIcon />\n {{ \"userProfile.lockedByAdmin\" | transloco }}\n </p>\n } @else if (editing) {\n <p\n class=\"m-0 text-(length:--size-sm) text-(--font-neutral-muted)\">\n {{ \"userProfile.editHint\" | transloco }}\n </p>\n } @else if (isDefault(category, key)) {\n <p class=\"m-0\">\n <span [class]=\"chipClass\">{{\n \"userProfile.defaultValue\" | transloco\n }}</span>\n </p>\n }\n }\n </div>\n }\n </div>\n </section>\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",
|
|
39123
39925
|
dependencies: [
|
|
39124
39926
|
{
|
|
39125
39927
|
kind: "directive",
|
|
@@ -39136,8 +39938,8 @@ var UserProfileFormComponent = class UserProfileFormComponent {
|
|
|
39136
39938
|
},
|
|
39137
39939
|
{
|
|
39138
39940
|
kind: "component",
|
|
39139
|
-
type:
|
|
39140
|
-
selector: "
|
|
39941
|
+
type: XMarkIcon,
|
|
39942
|
+
selector: "x-mark, x-mark-icon, XMark, XMarkIcon",
|
|
39141
39943
|
inputs: ["class"]
|
|
39142
39944
|
},
|
|
39143
39945
|
{
|
|
@@ -39323,7 +40125,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
39323
40125
|
FormRoot,
|
|
39324
40126
|
FormField,
|
|
39325
40127
|
TranslocoPipe,
|
|
39326
|
-
|
|
40128
|
+
XMarkIcon,
|
|
39327
40129
|
EditIcon,
|
|
39328
40130
|
UndoIcon,
|
|
39329
40131
|
SaveIcon,
|
|
@@ -39341,10 +40143,18 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
39341
40143
|
ChangePasswordComponent
|
|
39342
40144
|
],
|
|
39343
40145
|
providers: [provideTranslocoScope("user-profile", "login")],
|
|
39344
|
-
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 class=\"@container\"\n [formRoot]=\"profileForm\"\n [attr.aria-label]=\"'userProfile.profileFields' | transloco\"\n [attr.aria-busy]=\"profileForm().submitting() ? 'true' : null\">\n @for (section of sections(); track section.id) {\n <section\n [attr.aria-labelledby]=\"'user-profile-section-' + section.id\"\n class=\"mb-(--spacing-md) flex flex-col gap-(--spacing-sm) rounded-(--radius-lg) border border-solid border-(--stroke-primary-lighter) p-(--spacing-md)\">\n <div>\n <h3\n [id]=\"'user-profile-section-' + section.id\"\n class=\"m-0 text-(length:--size-base) font-semibold\">\n {{ \"userProfile.sections.\" + section.id | transloco }}\n </h3>\n <p\n class=\"m-0 text-(length:--size-sm) text-(--font-neutral-muted)\">\n {{\n \"userProfile.sections.\" + section.id + \"Hint\" | transloco\n }}\n </p>\n </div>\n <div\n class=\"grid grid-cols-1 gap-x-(--spacing-md) gap-y-(--spacing-sm) @lg:grid-cols-2\">\n @for (\n property of section.rows;\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 = labelFor(category, key);\n @let editing = isEditing(category, key);\n @let invalid = showError(field);\n @let boolean = isBoolean(category, key);\n @let readOnlyValue = isReadOnlyValue(category, key);\n @let leaf = isLeaf(category, key);\n @let saved = isSaved(category, key);\n <!-- role=\"group\" named by the label: without it the 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. A switch, and a value shown but not edited,\n take the whole width of the grid: they are not a field to line up with another. -->\n <div\n role=\"group\"\n [attr.aria-labelledby]=\"labelId(category, key)\"\n class=\"flex min-w-0 flex-col gap-(--spacing-3xs)\"\n [class.col-span-full]=\"boolean || readOnlyValue\">\n @if (boolean) {\n <!-- A switch is not a labelable text field: the name goes through aria-labelledby. The\n value is saved as soon as it is flipped, so there is no edit mode. -->\n <div\n class=\"flex flex-row items-center gap-(--spacing-sm) rounded-(--radius-md) border border-solid border-(--stroke-primary-lighter) px-(--spacing-md) py-(--spacing-xs)\">\n <span\n [id]=\"labelId(category, key)\"\n class=\"grow\"\n [class]=\"labelClass\"\n >{{ label }}</span\n >\n @if (saved) {\n <span\n class=\"flex items-center gap-(--spacing-3xs) text-(length:--size-sm) text-(--font-success-base)\">\n <CircleCheckIcon aria-hidden=\"true\" />\n {{ \"userProfile.saved\" | transloco }}\n </span>\n } @else if (isDefault(category, key)) {\n <span [class]=\"chipClass\">{{\n \"userProfile.defaultValue\" | transloco\n }}</span>\n }\n <switch\n #toggle\n [checked]=\"booleanValue(category, key)\"\n [disabled]=\"profileForm().submitting()\"\n [aria-labelledby]=\"labelId(category, key)\"\n (checkedChange)=\"\n onToggleBoolean(category, key, $event, toggle)\n \" />\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: { property: label }\n \"\n [disabled]=\"!hasStoredValue(category, key)\"\n (click)=\"onDeleteData(category, key)\">\n <TrashIcon />\n </button>\n </div>\n } @else if (readOnlyValue) {\n <!-- An array is shown, never edited: its shape is the administrator's. A galactik\n TagInput in read-only mode, which is what it is for; its inner text field is named\n after the property, as there is no <label for> to reach it. -->\n <span\n [id]=\"labelId(category, key)\"\n [class]=\"labelClass\"\n >{{ label }}</span\n >\n <tag-input\n class=\"[&_tag-remove]:hidden\"\n [value]=\"arrayTags(category, key)\"\n [readonly]=\"true\"\n [aria-label]=\"label\"\n [addLabel]=\"label\" />\n } @else {\n <label\n [id]=\"labelId(category, key)\"\n [for]=\"fieldId(category, key)\"\n [class]=\"labelClass\"\n >{{ label }}</label\n >\n <!-- The actions sit inside the box: galactik styles it from the control with has-[]\n selectors, which match descendants, so the focus ring and the read-only look\n keep working with the buttons next to the input. At rest the box is tinted,\n which is what tells a field from its label; it is white while edited. -->\n <!-- scheme: galactik colours the field itself for an error and for a save that just\n went through. The tint of a read-only field must not hide the success one, hence\n the data attribute and its more specific variant. -->\n <input-group\n class=\"has-[[readonly]]:bg-(--bg-primary-lighter) data-[state=success]:has-[[readonly]]:bg-(--bg-success-base-alt)\"\n [attr.data-state]=\"saved ? 'success' : null\"\n [scheme]=\"\n invalid ? 'error' : saved ? 'success' : 'default'\n \">\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 <input\n input-control\n type=\"text\"\n [id]=\"fieldId(category, key)\"\n [formField]=\"field\"\n [attr.placeholder]=\"\n 'userProfile.notDefined' | transloco\n \"\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)=\"onEscape($event, category, key)\" />\n <!-- Always visible: hiding them until hover made them unreachable on a touch screen\n and left nothing to discover. -->\n @if (!leaf) {\n <div class=\"flex shrink-0 flex-row items-center\">\n @if (editing) {\n <button\n type=\"submit\"\n variant=\"primary\"\n size=\"sm\"\n [iconOnly]=\"true\"\n class=\"cursor-pointer\"\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 size=\"sm\"\n [iconOnly]=\"true\"\n class=\"cursor-pointer\"\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 size=\"sm\"\n [iconOnly]=\"true\"\n [id]=\"editButtonId(category, key)\"\n class=\"cursor-pointer\"\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 size=\"sm\"\n [iconOnly]=\"true\"\n class=\"text-destructive cursor-pointer\"\n [title]=\"\n 'userProfile.deleteProperty' | transloco\n \"\n [attr.aria-label]=\"\n 'userProfile.deletePropertyNamed'\n | transloco: { property: label }\n \"\n [disabled]=\"!hasStoredValue(category, key)\"\n (click)=\"onDeleteData(category, key)\">\n <TrashIcon />\n </button>\n }\n </div>\n }\n </input-group>\n }\n <!-- Under the field, one message at a time: the error first, then the confirmation of a\n save, then what is worth knowing about the value. -->\n @if (invalid) {\n <p\n [id]=\"errorId(category, key)\"\n role=\"alert\"\n [class]=\"errorClass\">\n {{ errorKey(field) | transloco }}\n </p>\n } @else if (!boolean) {\n @if (saved) {\n <p\n class=\"m-0 flex items-center gap-(--spacing-3xs) text-(length:--size-sm) text-(--font-success-base)\">\n <CircleCheckIcon aria-hidden=\"true\" />\n {{ \"userProfile.saved\" | transloco }}\n </p>\n } @else if (readOnlyValue || leaf) {\n <p\n class=\"m-0 flex items-center gap-(--spacing-3xs) text-(length:--size-sm) text-(--font-neutral-muted)\">\n <LockIcon />\n {{ \"userProfile.lockedByAdmin\" | transloco }}\n </p>\n } @else if (editing) {\n <p\n class=\"m-0 text-(length:--size-sm) text-(--font-neutral-muted)\">\n {{ \"userProfile.editHint\" | transloco }}\n </p>\n } @else if (isDefault(category, key)) {\n <p class=\"m-0\">\n <span [class]=\"chipClass\">{{\n \"userProfile.defaultValue\" | transloco\n }}</span>\n </p>\n }\n }\n </div>\n }\n </div>\n </section>\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"
|
|
40146
|
+
template: "@if (userProfileResource.hasValue()) {\n @let profile = user();\n <div class=\"flex min-h-0 flex-1 flex-col gap-2\" [class.p-4]=\"padded()\">\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 <XMarkIcon />\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 class=\"@container\"\n [formRoot]=\"profileForm\"\n [attr.aria-label]=\"'userProfile.profileFields' | transloco\"\n [attr.aria-busy]=\"profileForm().submitting() ? 'true' : null\">\n @for (section of sections(); track section.id) {\n <section\n [attr.aria-labelledby]=\"'user-profile-section-' + section.id\"\n class=\"mb-(--spacing-md) flex flex-col gap-(--spacing-sm) rounded-(--radius-lg) border border-solid border-(--stroke-primary-lighter) p-(--spacing-md)\">\n <div>\n <h3\n [id]=\"'user-profile-section-' + section.id\"\n class=\"m-0 text-(length:--size-base) font-semibold\">\n {{ \"userProfile.sections.\" + section.id | transloco }}\n </h3>\n <p\n class=\"m-0 text-(length:--size-sm) text-(--font-neutral-muted)\">\n {{\n \"userProfile.sections.\" + section.id + \"Hint\" | transloco\n }}\n </p>\n </div>\n <div\n class=\"grid grid-cols-1 gap-x-(--spacing-md) gap-y-(--spacing-sm) @lg:grid-cols-2\">\n @for (\n property of section.rows;\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 = labelFor(category, key);\n @let editing = isEditing(category, key);\n @let invalid = showError(field);\n @let boolean = isBoolean(category, key);\n @let readOnlyValue = isReadOnlyValue(category, key);\n @let leaf = isLeaf(category, key);\n @let saved = isSaved(category, key);\n <!-- role=\"group\" named by the label: without it the 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. A switch, and a value shown but not edited,\n take the whole width of the grid: they are not a field to line up with another. -->\n <div\n role=\"group\"\n [attr.aria-labelledby]=\"labelId(category, key)\"\n class=\"flex min-w-0 flex-col gap-(--spacing-3xs)\"\n [class.col-span-full]=\"boolean || readOnlyValue\">\n @if (boolean) {\n <!-- A switch is not a labelable text field: the name goes through aria-labelledby. The\n value is saved as soon as it is flipped, so there is no edit mode. -->\n <div\n class=\"flex flex-row items-center gap-(--spacing-sm) rounded-(--radius-md) border border-solid border-(--stroke-primary-lighter) px-(--spacing-md) py-(--spacing-xs)\">\n <span\n [id]=\"labelId(category, key)\"\n class=\"grow\"\n [class]=\"labelClass\"\n >{{ label }}</span\n >\n @if (saved) {\n <span\n class=\"flex items-center gap-(--spacing-3xs) text-(length:--size-sm) text-(--font-success-base)\">\n <CircleCheckIcon aria-hidden=\"true\" />\n {{ \"userProfile.saved\" | transloco }}\n </span>\n } @else if (isDefault(category, key)) {\n <span [class]=\"chipClass\">{{\n \"userProfile.defaultValue\" | transloco\n }}</span>\n }\n <switch\n #toggle\n [checked]=\"booleanValue(category, key)\"\n [disabled]=\"profileForm().submitting()\"\n [aria-labelledby]=\"labelId(category, key)\"\n (checkedChange)=\"\n onToggleBoolean(category, key, $event, toggle)\n \" />\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: { property: label }\n \"\n [disabled]=\"!hasStoredValue(category, key)\"\n (click)=\"onDeleteData(category, key)\">\n <XMarkIcon />\n </button>\n </div>\n } @else if (readOnlyValue) {\n <!-- An array is shown, never edited: its shape is the administrator's. A galactik\n TagInput in read-only mode, which is what it is for; its inner text field is named\n after the property, as there is no <label for> to reach it. -->\n <span\n [id]=\"labelId(category, key)\"\n [class]=\"labelClass\"\n >{{ label }}</span\n >\n <tag-input\n class=\"[&_tag-remove]:hidden\"\n [value]=\"arrayTags(category, key)\"\n [readonly]=\"true\"\n [aria-label]=\"label\"\n [addLabel]=\"label\" />\n } @else {\n <label\n [id]=\"labelId(category, key)\"\n [for]=\"fieldId(category, key)\"\n [class]=\"labelClass\"\n >{{ label }}</label\n >\n <!-- The actions sit inside the box: galactik styles it from the control with has-[]\n selectors, which match descendants, so the focus ring and the read-only look\n keep working with the buttons next to the input. At rest the box is tinted,\n which is what tells a field from its label; it is white while edited. -->\n <!-- scheme: galactik colours the field itself for an error and for a save that just\n went through. The tint of a read-only field must not hide the success one, hence\n the data attribute and its more specific variant. -->\n <input-group\n class=\"has-[[readonly]]:bg-(--bg-primary-lighter) data-[state=success]:has-[[readonly]]:bg-(--bg-success-base-alt)\"\n [attr.data-state]=\"saved ? 'success' : null\"\n [scheme]=\"\n invalid ? 'error' : saved ? 'success' : 'default'\n \">\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 <input\n input-control\n type=\"text\"\n [id]=\"fieldId(category, key)\"\n [formField]=\"field\"\n [attr.placeholder]=\"\n 'userProfile.notDefined' | transloco\n \"\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)=\"onEscape($event, category, key)\" />\n <!-- Always visible: hiding them until hover made them unreachable on a touch screen\n and left nothing to discover. -->\n @if (!leaf) {\n <div class=\"flex shrink-0 flex-row items-center\">\n @if (editing) {\n <button\n type=\"submit\"\n variant=\"primary\"\n size=\"sm\"\n [iconOnly]=\"true\"\n class=\"cursor-pointer\"\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 size=\"sm\"\n [iconOnly]=\"true\"\n class=\"cursor-pointer\"\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 size=\"sm\"\n [iconOnly]=\"true\"\n [id]=\"editButtonId(category, key)\"\n class=\"cursor-pointer\"\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 size=\"sm\"\n [iconOnly]=\"true\"\n class=\"text-destructive cursor-pointer\"\n [title]=\"\n 'userProfile.deleteProperty' | transloco\n \"\n [attr.aria-label]=\"\n 'userProfile.deletePropertyNamed'\n | transloco: { property: label }\n \"\n [disabled]=\"!hasStoredValue(category, key)\"\n (click)=\"onDeleteData(category, key)\">\n <XMarkIcon />\n </button>\n }\n </div>\n }\n </input-group>\n }\n <!-- Under the field, one message at a time: the error first, then the confirmation of a\n save, then what is worth knowing about the value. -->\n @if (invalid) {\n <p\n [id]=\"errorId(category, key)\"\n role=\"alert\"\n [class]=\"errorClass\">\n {{ errorKey(field) | transloco }}\n </p>\n } @else if (!boolean) {\n @if (saved) {\n <p\n class=\"m-0 flex items-center gap-(--spacing-3xs) text-(length:--size-sm) text-(--font-success-base)\">\n <CircleCheckIcon aria-hidden=\"true\" />\n {{ \"userProfile.saved\" | transloco }}\n </p>\n } @else if (readOnlyValue || leaf) {\n <p\n class=\"m-0 flex items-center gap-(--spacing-3xs) text-(length:--size-sm) text-(--font-neutral-muted)\">\n <LockIcon />\n {{ \"userProfile.lockedByAdmin\" | transloco }}\n </p>\n } @else if (editing) {\n <p\n class=\"m-0 text-(length:--size-sm) text-(--font-neutral-muted)\">\n {{ \"userProfile.editHint\" | transloco }}\n </p>\n } @else if (isDefault(category, key)) {\n <p class=\"m-0\">\n <span [class]=\"chipClass\">{{\n \"userProfile.defaultValue\" | transloco\n }}</span>\n </p>\n }\n }\n </div>\n }\n </div>\n </section>\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"
|
|
39345
40147
|
}]
|
|
39346
40148
|
}],
|
|
39347
|
-
ctorParameters: () => []
|
|
40149
|
+
ctorParameters: () => [],
|
|
40150
|
+
propDecorators: { padded: [{
|
|
40151
|
+
type: i0.Input,
|
|
40152
|
+
args: [{
|
|
40153
|
+
isSignal: true,
|
|
40154
|
+
alias: "padded",
|
|
40155
|
+
required: false
|
|
40156
|
+
}]
|
|
40157
|
+
}] }
|
|
39348
40158
|
});
|
|
39349
40159
|
function toRecord(keys, source) {
|
|
39350
40160
|
return Object.fromEntries(keys.map((key) => [key, toFieldText(source?.[key])]));
|
|
@@ -39417,7 +40227,7 @@ var SettingsAccountPanelComponent = class SettingsAccountPanelComponent {
|
|
|
39417
40227
|
<p class="text-(--font-primary-muted)">{{ "settings.account.description" | transloco }}</p>
|
|
39418
40228
|
|
|
39419
40229
|
@if (profileEnabled()) {
|
|
39420
|
-
<user-profile-form class="mt-(--spacing-md) min-h-0 flex-1" />
|
|
40230
|
+
<user-profile-form class="mt-(--spacing-md) min-h-0 flex-1" [padded]="false" />
|
|
39421
40231
|
} @else if (allowChangePassword()) {
|
|
39422
40232
|
<!-- No profile to show: the password is all the account offers, as it was in the user menu. -->
|
|
39423
40233
|
<change-password
|
|
@@ -39488,7 +40298,8 @@ var SettingsAccountPanelComponent = class SettingsAccountPanelComponent {
|
|
|
39488
40298
|
{
|
|
39489
40299
|
kind: "component",
|
|
39490
40300
|
type: UserProfileFormComponent,
|
|
39491
|
-
selector: "user-profile-form, UserProfileForm, userprofileform"
|
|
40301
|
+
selector: "user-profile-form, UserProfileForm, userprofileform",
|
|
40302
|
+
inputs: ["padded"]
|
|
39492
40303
|
},
|
|
39493
40304
|
{
|
|
39494
40305
|
kind: "component",
|
|
@@ -39537,7 +40348,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
39537
40348
|
<p class="text-(--font-primary-muted)">{{ "settings.account.description" | transloco }}</p>
|
|
39538
40349
|
|
|
39539
40350
|
@if (profileEnabled()) {
|
|
39540
|
-
<user-profile-form class="mt-(--spacing-md) min-h-0 flex-1" />
|
|
40351
|
+
<user-profile-form class="mt-(--spacing-md) min-h-0 flex-1" [padded]="false" />
|
|
39541
40352
|
} @else if (allowChangePassword()) {
|
|
39542
40353
|
<!-- No profile to show: the password is all the account offers, as it was in the user menu. -->
|
|
39543
40354
|
<change-password
|
|
@@ -40014,6 +40825,17 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
40014
40825
|
}]
|
|
40015
40826
|
}]
|
|
40016
40827
|
});
|
|
40828
|
+
function revealInStrip(strip, item, behavior = "smooth") {
|
|
40829
|
+
const stripBox = strip.getBoundingClientRect();
|
|
40830
|
+
const itemBox = item.getBoundingClientRect();
|
|
40831
|
+
const scale = strip.offsetWidth ? stripBox.width / strip.offsetWidth : 1;
|
|
40832
|
+
const delta = (itemBox.left - stripBox.left - (stripBox.width - itemBox.width) / 2) / scale;
|
|
40833
|
+
if (Math.abs(delta) < 1) return;
|
|
40834
|
+
strip.scrollBy({
|
|
40835
|
+
left: delta,
|
|
40836
|
+
behavior
|
|
40837
|
+
});
|
|
40838
|
+
}
|
|
40017
40839
|
var SettingsShortcutsPanelComponent = class SettingsShortcutsPanelComponent {
|
|
40018
40840
|
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
40019
40841
|
minVersion: "12.0.0",
|
|
@@ -40071,6 +40893,8 @@ let active;
|
|
|
40071
40893
|
var SettingsDialogComponent = class SettingsDialogComponent {
|
|
40072
40894
|
call = injectCallRef();
|
|
40073
40895
|
dialog = viewChild(DialogComponent, ...ngDevMode ? [{ debugName: "dialog" }] : /* istanbul ignore next */ []);
|
|
40896
|
+
strip = viewChild("strip", ...ngDevMode ? [{ debugName: "strip" }] : /* istanbul ignore next */ []);
|
|
40897
|
+
stripPositioned = false;
|
|
40074
40898
|
items = [
|
|
40075
40899
|
{
|
|
40076
40900
|
id: "account",
|
|
@@ -40090,7 +40914,26 @@ var SettingsDialogComponent = class SettingsDialogComponent {
|
|
|
40090
40914
|
}
|
|
40091
40915
|
];
|
|
40092
40916
|
panel = signal(this.call.props()?.panel ?? "account", ...ngDevMode ? [{ debugName: "panel" }] : /* istanbul ignore next */ []);
|
|
40917
|
+
keyboardAvailable = injectKeyboardAvailable();
|
|
40918
|
+
visibleItems = computed(() => this.items.filter((item) => item.id !== "shortcuts" || this.keyboardAvailable() || this.panel() === "shortcuts"), ...ngDevMode ? [{ debugName: "visibleItems" }] : /* istanbul ignore next */ []);
|
|
40093
40919
|
navigated = signal(false, ...ngDevMode ? [{ debugName: "navigated" }] : /* istanbul ignore next */ []);
|
|
40920
|
+
resizeObserver;
|
|
40921
|
+
reveal(strip, current) {
|
|
40922
|
+
const reduced = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
|
|
40923
|
+
revealInStrip(strip, current, this.stripPositioned && !reduced ? "smooth" : "auto");
|
|
40924
|
+
this.stripPositioned = true;
|
|
40925
|
+
}
|
|
40926
|
+
revealWhenLaidOut(strip) {
|
|
40927
|
+
if (this.resizeObserver || typeof ResizeObserver === "undefined") return;
|
|
40928
|
+
this.resizeObserver = new ResizeObserver(() => {
|
|
40929
|
+
if (!strip.clientWidth) return;
|
|
40930
|
+
this.resizeObserver?.disconnect();
|
|
40931
|
+
this.resizeObserver = void 0;
|
|
40932
|
+
const current = strip.querySelector("[aria-current=\"page\"]");
|
|
40933
|
+
if (current) this.reveal(strip, current);
|
|
40934
|
+
});
|
|
40935
|
+
this.resizeObserver.observe(strip);
|
|
40936
|
+
}
|
|
40094
40937
|
navigate(id) {
|
|
40095
40938
|
this.navigated.set(true);
|
|
40096
40939
|
this.panel.set(id);
|
|
@@ -40103,7 +40946,18 @@ var SettingsDialogComponent = class SettingsDialogComponent {
|
|
|
40103
40946
|
return;
|
|
40104
40947
|
}
|
|
40105
40948
|
active = this;
|
|
40949
|
+
afterRenderEffect({ read: () => {
|
|
40950
|
+
this.panel();
|
|
40951
|
+
this.visibleItems();
|
|
40952
|
+
if (!this.dialog()?.isOpen()) return;
|
|
40953
|
+
const strip = this.strip()?.nativeElement;
|
|
40954
|
+
const current = strip?.querySelector("[aria-current=\"page\"]");
|
|
40955
|
+
if (!strip || !current) return;
|
|
40956
|
+
if (!strip.clientWidth) this.revealWhenLaidOut(strip);
|
|
40957
|
+
else this.reveal(strip, current);
|
|
40958
|
+
} });
|
|
40106
40959
|
inject(DestroyRef).onDestroy(() => {
|
|
40960
|
+
this.resizeObserver?.disconnect();
|
|
40107
40961
|
if (active === this) active = void 0;
|
|
40108
40962
|
});
|
|
40109
40963
|
afterNextRender(() => this.dialog()?.open());
|
|
@@ -40129,6 +40983,12 @@ var SettingsDialogComponent = class SettingsDialogComponent {
|
|
|
40129
40983
|
predicate: DialogComponent,
|
|
40130
40984
|
descendants: true,
|
|
40131
40985
|
isSignal: true
|
|
40986
|
+
}, {
|
|
40987
|
+
propertyName: "strip",
|
|
40988
|
+
first: true,
|
|
40989
|
+
predicate: ["strip"],
|
|
40990
|
+
descendants: true,
|
|
40991
|
+
isSignal: true
|
|
40132
40992
|
}],
|
|
40133
40993
|
ngImport: i0,
|
|
40134
40994
|
template: `
|
|
@@ -40136,12 +40996,14 @@ var SettingsDialogComponent = class SettingsDialogComponent {
|
|
|
40136
40996
|
<div dialog #dialog="dialog" [closeLabel]="'settings.close' | transloco" (closed)="call.end($event)">
|
|
40137
40997
|
<!-- The inset layout of the sidebar, in a dialog: the navigation sits on the tinted surface of the
|
|
40138
40998
|
dialog itself and the panel is a white card set into it. -->
|
|
40139
|
-
|
|
40140
|
-
|
|
40999
|
+
<!-- Under md (a phone) the navigation does not fit beside the panel, which was left 120px wide: it becomes a strip on top, that scrolls
|
|
41000
|
+
sideways, and the panel takes the rest of the height. -->
|
|
41001
|
+
<DialogContent size="xl" class="h-[min(48rem,calc(100dvh-2rem))] flex-row! items-stretch! bg-(--bg-primary-lighter) max-md:flex-col!">
|
|
41002
|
+
<div class="flex w-64 shrink-0 flex-col gap-(--spacing-3xs) p-(--spacing-sm) max-md:w-full max-md:flex-row max-md:items-center max-md:gap-(--spacing-2xs) max-md:p-(--spacing-xs)">
|
|
40141
41003
|
<button
|
|
40142
41004
|
type="button"
|
|
40143
41005
|
variant="none"
|
|
40144
|
-
class="modal-close mb-(--spacing-sm)"
|
|
41006
|
+
class="modal-close mb-(--spacing-sm) max-md:mb-0 max-md:shrink-0"
|
|
40145
41007
|
[attr.aria-label]="'settings.close' | transloco"
|
|
40146
41008
|
(click)="dialog.close()">
|
|
40147
41009
|
<XMarkIcon aria-hidden="true" />
|
|
@@ -40149,12 +41011,15 @@ var SettingsDialogComponent = class SettingsDialogComponent {
|
|
|
40149
41011
|
|
|
40150
41012
|
<!-- Panels are a navigation, not a tablist: each one is a button, the current one is
|
|
40151
41013
|
flagged with aria-current, which is what a screen reader reads out. -->
|
|
40152
|
-
<nav
|
|
40153
|
-
|
|
41014
|
+
<nav
|
|
41015
|
+
#strip
|
|
41016
|
+
[attr.aria-label]="'settings.title' | transloco"
|
|
41017
|
+
class="flex flex-col gap-(--spacing-3xs) max-md:min-w-0 max-md:flex-1 max-md:flex-row max-md:overflow-x-auto max-md:[scrollbar-width:none]">
|
|
41018
|
+
@for (item of visibleItems(); track item.id) {
|
|
40154
41019
|
<button
|
|
40155
41020
|
type="button"
|
|
40156
41021
|
variant="none"
|
|
40157
|
-
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)"
|
|
41022
|
+
class="flex h-(--row-height-md) w-full max-md:w-auto max-md:shrink-0 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)"
|
|
40158
41023
|
[attr.aria-current]="panel() === item.id ? 'page' : null"
|
|
40159
41024
|
(click)="navigate(item.id)">
|
|
40160
41025
|
@switch (item.id) {
|
|
@@ -40177,7 +41042,7 @@ var SettingsDialogComponent = class SettingsDialogComponent {
|
|
|
40177
41042
|
</nav>
|
|
40178
41043
|
</div>
|
|
40179
41044
|
|
|
40180
|
-
<div sidebarInset class="my-(--spacing-xs) mr-(--spacing-xs) overflow-y-auto rounded-(--radius-2xl) bg-(--bg-neutral-white) p-(--spacing-xl) shadow-none">
|
|
41045
|
+
<div sidebarInset class="my-(--spacing-xs) mr-(--spacing-xs) min-h-0 overflow-y-auto max-md:m-(--spacing-xs) max-md:mt-0 max-md:min-w-0 max-md:flex-1 rounded-(--radius-2xl) bg-(--bg-neutral-white) p-(--spacing-xl) shadow-none">
|
|
40181
41046
|
@switch (panel()) {
|
|
40182
41047
|
@case ("account") {
|
|
40183
41048
|
<settings-account-panel class="flex min-h-0 flex-1 flex-col" [focusOnOpen]="!navigated()" />
|
|
@@ -40326,12 +41191,14 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
40326
41191
|
<div dialog #dialog="dialog" [closeLabel]="'settings.close' | transloco" (closed)="call.end($event)">
|
|
40327
41192
|
<!-- The inset layout of the sidebar, in a dialog: the navigation sits on the tinted surface of the
|
|
40328
41193
|
dialog itself and the panel is a white card set into it. -->
|
|
40329
|
-
|
|
40330
|
-
|
|
41194
|
+
<!-- Under md (a phone) the navigation does not fit beside the panel, which was left 120px wide: it becomes a strip on top, that scrolls
|
|
41195
|
+
sideways, and the panel takes the rest of the height. -->
|
|
41196
|
+
<DialogContent size="xl" class="h-[min(48rem,calc(100dvh-2rem))] flex-row! items-stretch! bg-(--bg-primary-lighter) max-md:flex-col!">
|
|
41197
|
+
<div class="flex w-64 shrink-0 flex-col gap-(--spacing-3xs) p-(--spacing-sm) max-md:w-full max-md:flex-row max-md:items-center max-md:gap-(--spacing-2xs) max-md:p-(--spacing-xs)">
|
|
40331
41198
|
<button
|
|
40332
41199
|
type="button"
|
|
40333
41200
|
variant="none"
|
|
40334
|
-
class="modal-close mb-(--spacing-sm)"
|
|
41201
|
+
class="modal-close mb-(--spacing-sm) max-md:mb-0 max-md:shrink-0"
|
|
40335
41202
|
[attr.aria-label]="'settings.close' | transloco"
|
|
40336
41203
|
(click)="dialog.close()">
|
|
40337
41204
|
<XMarkIcon aria-hidden="true" />
|
|
@@ -40339,12 +41206,15 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
40339
41206
|
|
|
40340
41207
|
<!-- Panels are a navigation, not a tablist: each one is a button, the current one is
|
|
40341
41208
|
flagged with aria-current, which is what a screen reader reads out. -->
|
|
40342
|
-
<nav
|
|
40343
|
-
|
|
41209
|
+
<nav
|
|
41210
|
+
#strip
|
|
41211
|
+
[attr.aria-label]="'settings.title' | transloco"
|
|
41212
|
+
class="flex flex-col gap-(--spacing-3xs) max-md:min-w-0 max-md:flex-1 max-md:flex-row max-md:overflow-x-auto max-md:[scrollbar-width:none]">
|
|
41213
|
+
@for (item of visibleItems(); track item.id) {
|
|
40344
41214
|
<button
|
|
40345
41215
|
type="button"
|
|
40346
41216
|
variant="none"
|
|
40347
|
-
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)"
|
|
41217
|
+
class="flex h-(--row-height-md) w-full max-md:w-auto max-md:shrink-0 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)"
|
|
40348
41218
|
[attr.aria-current]="panel() === item.id ? 'page' : null"
|
|
40349
41219
|
(click)="navigate(item.id)">
|
|
40350
41220
|
@switch (item.id) {
|
|
@@ -40367,7 +41237,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
40367
41237
|
</nav>
|
|
40368
41238
|
</div>
|
|
40369
41239
|
|
|
40370
|
-
<div sidebarInset class="my-(--spacing-xs) mr-(--spacing-xs) overflow-y-auto rounded-(--radius-2xl) bg-(--bg-neutral-white) p-(--spacing-xl) shadow-none">
|
|
41240
|
+
<div sidebarInset class="my-(--spacing-xs) mr-(--spacing-xs) min-h-0 overflow-y-auto max-md:m-(--spacing-xs) max-md:mt-0 max-md:min-w-0 max-md:flex-1 rounded-(--radius-2xl) bg-(--bg-neutral-white) p-(--spacing-xl) shadow-none">
|
|
40371
41241
|
@switch (panel()) {
|
|
40372
41242
|
@case ("account") {
|
|
40373
41243
|
<settings-account-panel class="flex min-h-0 flex-1 flex-col" [focusOnOpen]="!navigated()" />
|
|
@@ -40390,10 +41260,16 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
40390
41260
|
}]
|
|
40391
41261
|
}],
|
|
40392
41262
|
ctorParameters: () => [],
|
|
40393
|
-
propDecorators: {
|
|
40394
|
-
|
|
40395
|
-
|
|
40396
|
-
|
|
41263
|
+
propDecorators: {
|
|
41264
|
+
dialog: [{
|
|
41265
|
+
type: i0.ViewChild,
|
|
41266
|
+
args: [i0.forwardRef(() => DialogComponent), { isSignal: true }]
|
|
41267
|
+
}],
|
|
41268
|
+
strip: [{
|
|
41269
|
+
type: i0.ViewChild,
|
|
41270
|
+
args: ["strip", { isSignal: true }]
|
|
41271
|
+
}]
|
|
41272
|
+
}
|
|
40397
41273
|
});
|
|
40398
41274
|
const Settings = createCallable(SettingsDialogComponent);
|
|
40399
41275
|
const auditInterceptorFn = (request, next) => {
|