@sinequa/atomic-angular 2.6.0 → 2.8.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/README.md +11 -2
- package/features/alerts/i18n/de.json +10 -0
- package/features/alerts/i18n/en.json +10 -0
- package/features/alerts/i18n/fr.json +10 -0
- package/features/bookmarks/i18n/de.json +1 -0
- package/features/bookmarks/i18n/en.json +1 -0
- package/features/bookmarks/i18n/fr.json +1 -0
- package/features/collections/i18n/de.json +4 -1
- package/features/collections/i18n/en.json +4 -1
- package/features/collections/i18n/fr.json +4 -1
- package/features/did-you-mean/i18n/de.json +2 -1
- package/features/did-you-mean/i18n/en.json +2 -1
- package/features/did-you-mean/i18n/fr.json +2 -1
- package/features/searches/i18n/de.json +2 -0
- package/features/searches/i18n/en.json +2 -0
- package/features/searches/i18n/fr.json +2 -0
- package/features/user-profile/i18n/de.json +10 -0
- package/features/user-profile/i18n/en.json +10 -0
- package/features/user-profile/i18n/fr.json +10 -0
- package/fesm2022/sinequa-atomic-angular.mjs +862 -300
- package/fesm2022/sinequa-atomic-angular.mjs.map +1 -1
- package/package.json +1 -1
- package/types/sinequa-atomic-angular.d.ts +474 -342
|
@@ -2,11 +2,11 @@ import * as i0 from "@angular/core";
|
|
|
2
2
|
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, DOCUMENT, DestroyRef, Directive, ElementRef, EnvironmentInjector, HostBinding, HostListener, Inject, Injectable, InjectionToken, Injector, LOCALE_ID, Optional, Pipe, Service, TemplateRef, ViewEncapsulation, afterNextRender, afterRenderEffect, assertInInjectionContext, booleanAttribute, computed, contentChild, contentChildren, effect, inject, input, isDevMode, linkedSignal, makeEnvironmentProviders, model, numberAttribute, output, provideEnvironmentInitializer, resource, runInInjectionContext, signal, untracked, viewChild, viewChildren } from "@angular/core";
|
|
3
3
|
import { BehaviorSubject, EMPTY, Subject, Subscription, catchError, debounceTime, filter, firstValueFrom, from, fromEvent, map, of, scan, shareReplay, switchMap, tap, throwError } from "rxjs";
|
|
4
4
|
import { TranslocoPipe, TranslocoService, provideTranslocoScope } from "@jsverse/transloco";
|
|
5
|
-
import { ArrowDownIcon, ArrowDownZaIcon, ArrowLeftIcon, ArrowRightFromBracketIcon, ArrowRightIcon, ArrowUpAzIcon, ArrowUpIcon, ArrowUpRightFromSquareIcon, ArrowsMaximizeIcon, AutocompleteComponent, AvatarComponent, AvatarFallbackComponent, AvatarImageComponent, BadgeComponent, BarsIcon, BellIcon, BookmarkIcon, BreakpointObserverService, ButtonComponent, CalendarDayIcon, CheckIcon, CheckboxComponent, ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon, ChevronsLeftIcon, ChevronsRightIcon, CircleCheckIcon, CommentIcon, CommentsIcon, DIALOG_REF, DesktopIcon, DialogBodyComponent, DialogComponent, DialogContentComponent, DialogDescriptionComponent, DialogFooterComponent, DialogHeaderComponent, DialogTitleComponent, DropdownDirective, EditIcon, EllipsisIcon, EllipsisVerticalIcon, EyeIcon, EyeSlashIcon, FileHTMLIcon, FileIcon, FileOutputIcon, FileSpreadsheetIcon, FileTextIcon, FilterIcon, FilterXIcon, FlagEnglishIcon, FlagFrenchIcon, FlagGermanIcon, FlashlightIcon, FolderIcon, FolderOpenIcon, FrownIcon, GearIcon, GitForkIcon, GlobeIcon, HighlighterIcon, HistoryIcon, HomeIcon, ImageIcon, InboxIcon, InfoCircleIcon, InputComponent, InputControlDirective, InputIconRightDirective, KbdComponent, KbdGroupComponent, KeyIcon, KeyboardIcon, LanguageIcon, LightbulbIcon, LightbulbSlashIcon, LinkComponent, ListComponent, ListFilterIcon, ListItemActionComponent, ListItemAvatarDirective, ListItemComponent, ListItemLeftDirective, ListItemTextComponent, LoadingCircleIcon, MagnifyingGlassIcon, MagnifyingGlassMinusIcon, MagnifyingGlassPlusIcon, MenuComponent, MenuContentComponent, MenuItemComponent, MenuTriggerDirective, MoonIcon, PaletteIcon, PaperclipIcon, PenToSquareIcon, PlusIcon, PopoverDirective, PresentationIcon, QuestionCircleIcon, RadioComponent, RobotIcon, SaveIcon, SearchBarComponent, SearchInputComponent, SelectComponent, SelectTriggerDirective, SendIcon, SheetBodyComponent, SheetComponent, SheetHeaderComponent, SlashIcon, SlidersIcon, SparklesIcon, SpinnerIcon, SquareCheckIcon, SquareIcon, SquareMinusIcon, StarIcon, SunBrightIcon, SwitchComponent, SyncIcon, TabComponent, TabListComponent, TabPanelComponent, TabsComponent, TagComponent, TagIcon, TagsIcon, TextareaComponent, TextareaControlDirective, ThumbsDownIcon, ThumbsUpIcon, ToggleComponent, ToggleGroupComponent, ToggleItemComponent, ToggleLeftIcon, ToggleRightIcon, TooltipDirective, TrashCanIcon, TrashIcon, TreeComponent, TreeItemComponent, TreeItemExpanderComponent, TreeItemGroupComponent, TriangleAlertIcon, UndoIcon, UserIcon, UserSecretIcon, VideoIcon, XMarkIcon, createCallable, injectCallRef, 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, ButtonComponent, CalendarDayIcon, CheckIcon, CheckboxComponent, ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon, ChevronsLeftIcon, ChevronsRightIcon, CircleCheckIcon, CommentIcon, CommentsIcon, DIALOG_REF, DesktopIcon, DialogBodyComponent, DialogComponent, DialogContentComponent, DialogDescriptionComponent, DialogFooterComponent, DialogHeaderComponent, DialogTitleComponent, DropdownDirective, EditIcon, EllipsisIcon, EllipsisVerticalIcon, EyeIcon, EyeSlashIcon, FileHTMLIcon, FileIcon, FileOutputIcon, FileSpreadsheetIcon, FileTextIcon, FilterIcon, FilterXIcon, FlagEnglishIcon, FlagFrenchIcon, FlagGermanIcon, FlashlightIcon, FolderIcon, FolderOpenIcon, FrownIcon, GearIcon, GitForkIcon, GlobeIcon, GripVerticalIcon, HighlighterIcon, HistoryIcon, HomeIcon, ImageIcon, InboxIcon, InfoCircleIcon, InputComponent, InputControlDirective, InputIconRightDirective, KbdComponent, KbdGroupComponent, KeyIcon, KeyboardIcon, LanguageIcon, LightbulbIcon, LightbulbSlashIcon, LinkComponent, ListComponent, ListFilterIcon, ListItemActionComponent, ListItemAvatarDirective, ListItemComponent, ListItemLeftDirective, ListItemTextComponent, LoadingCircleIcon, LockIcon, MagnifyingGlassIcon, MagnifyingGlassMinusIcon, MagnifyingGlassPlusIcon, MenuComponent, MenuContentComponent, MenuItemComponent, MenuTriggerDirective, MoonIcon, PaletteIcon, PaperclipIcon, PenToSquareIcon, PlusIcon, PopoverDirective, PresentationIcon, QuestionCircleIcon, RadioComponent, RobotIcon, SaveIcon, SearchBarComponent, SearchIcon, SearchInputComponent, SelectComponent, SelectTriggerDirective, SendIcon, SheetBodyComponent, SheetComponent, SheetHeaderComponent, SidebarInsetDirective, SlashIcon, SlidersIcon, SortableDirective, SortableItemDirective, SparklesIcon, SpinnerIcon, SquareCheckIcon, SquareIcon, SquareMinusIcon, StarIcon, SunBrightIcon, SwitchComponent, SyncIcon, TabComponent, TabListComponent, TabPanelComponent, TabsComponent, TagComponent, TagIcon, TagInputComponent, TagsIcon, TextareaComponent, TextareaControlDirective, ThumbsDownIcon, ThumbsUpIcon, ToggleComponent, ToggleGroupComponent, ToggleItemComponent, ToggleLeftIcon, ToggleRightIcon, TooltipDirective, TrashCanIcon, TrashIcon, TreeComponent, TreeItemComponent, TreeItemExpanderComponent, TreeItemGroupComponent, TriangleAlertIcon, TruncatedDirective, UndoIcon, UserIcon, UserSecretIcon, VideoIcon, XMarkIcon, createCallable, injectCallRef, isApplePlatform } from "@sinequa/galactik";
|
|
6
6
|
import { withDevtools } from "@angular-architects/ngrx-toolkit";
|
|
7
7
|
import { getState, patchState, signalStore, signalStoreFeature, withComputed, withMethods, withState } from "@ngrx/signals";
|
|
8
8
|
import { ActivatedRoute, NavigationEnd, Router, RouterLink, RouterModule } from "@angular/router";
|
|
9
|
-
import { Audit, AuditEventType, EngineType, addAuditAdditionalInfo, addConcepts, aggItemRegex, bisect, buildPathsAndLevels, createAtomicClient, createHeaders, debug, error, escapeExpr, expiresSoon, extraColumns, filter as filter$1, flattenLegacyFilterValues, getDefaultClient, getMetadata, getQueryParamsFromUrl, getRelativeDate, getToken, getUrlParamsFromQueryParams, globalConfig, info, isExpired, isExternalAuthMode, isJsonable, isNotInputEvent, isObject, notify, parseValueAndOperatorFromItem,
|
|
9
|
+
import { Audit, AuditEventType, EngineType, addAuditAdditionalInfo, addConcepts, aggItemRegex, bisect, buildPathsAndLevels, createAtomicClient, createHeaders, debug, error, escapeExpr, expiresSoon, extraColumns, filter as filter$1, flattenLegacyFilterValues, getDefaultClient, getMetadata, getQueryParamsFromUrl, getRelativeDate, getToken, getUrlParamsFromQueryParams, globalConfig, info, isExpired, isExternalAuthMode, isJsonable, isNotInputEvent, isObject, notify, parseValueAndOperatorFromItem, resolveBearerToken, resolveOAuthProvider, resolveSAMLProvider, resultPages, setToken, suggestionsToTreeAggregationNodes, sysLang, translateAggregationToDateOptions, warn } from "@sinequa/atomic";
|
|
10
10
|
import { DomSanitizer, Title } from "@angular/platform-browser";
|
|
11
11
|
import MarkdownIt from "markdown-it";
|
|
12
12
|
import { DATE_PIPE_DEFAULT_OPTIONS, DATE_PIPE_DEFAULT_TIMEZONE, DatePipe, Location, NgComponentOutlet, NgStyle, NgTemplateOutlet } from "@angular/common";
|
|
@@ -18,8 +18,6 @@ import { FormField, FormRoot, applyEach, form, readonly, required, schema, submi
|
|
|
18
18
|
import { injectVirtualizer } from "@tanstack/angular-virtual";
|
|
19
19
|
import { catchError as catchError$1, filter as filter$2, map as map$1, switchMap as switchMap$1 } from "rxjs/operators";
|
|
20
20
|
import { Tab } from "@angular/aria/tabs";
|
|
21
|
-
import * as i1 from "@angular/cdk/drag-drop";
|
|
22
|
-
import { DragDropModule } from "@angular/cdk/drag-drop";
|
|
23
21
|
import { A11yModule } from "@angular/cdk/a11y";
|
|
24
22
|
import { Calendar, months } from "vanilla-calendar-pro";
|
|
25
23
|
import { getDate, getDateString } from "vanilla-calendar-pro/utils";
|
|
@@ -296,8 +294,9 @@ function withAppFeatures() {
|
|
|
296
294
|
};
|
|
297
295
|
},
|
|
298
296
|
getQueryByName(name) {
|
|
297
|
+
if (!name) return void 0;
|
|
299
298
|
const queries = store.queries();
|
|
300
|
-
return Object.values(queries).find((query) => query.name
|
|
299
|
+
return Object.values(queries).find((query) => query.name?.toLowerCase() === name.toLowerCase());
|
|
301
300
|
},
|
|
302
301
|
getQueryByIndex(index) {
|
|
303
302
|
const queries = store.queries();
|
|
@@ -805,6 +804,9 @@ var QueryService = class QueryService {
|
|
|
805
804
|
result = signal({}, ...ngDevMode ? [{ debugName: "result" }] : /* istanbul ignore next */ []);
|
|
806
805
|
failure = computed(() => this.result().$error, ...ngDevMode ? [{ debugName: "failure" }] : /* istanbul ignore next */ []);
|
|
807
806
|
audit;
|
|
807
|
+
warnEmptySearch() {
|
|
808
|
+
firstValueFrom(this.transloco.selectTranslate("emptySearch", {}, "searches")).catch(() => "Please enter a search query.").then((message) => notify.warning(message));
|
|
809
|
+
}
|
|
808
810
|
search(q, includeQueryParams = true, audit) {
|
|
809
811
|
const $auditRecord = { auditEvents: [audit ?? this.audit ?? {
|
|
810
812
|
type: "Search_Text",
|
|
@@ -820,7 +822,7 @@ var QueryService = class QueryService {
|
|
|
820
822
|
if (!((this.queryParamsStore.filters?.() || []).length > 0) && !query.basket && query.action !== "open") {
|
|
821
823
|
if (this.appStore.allowEmptySearch(query?.name || "") === false && query?.text === "") {
|
|
822
824
|
warn("QueryService.search: Empty search query is not allowed. Returning empty result.");
|
|
823
|
-
|
|
825
|
+
this.warnEmptySearch();
|
|
824
826
|
const emptyResult = { records: [] };
|
|
825
827
|
this.result.set(emptyResult);
|
|
826
828
|
return of(emptyResult);
|
|
@@ -1370,9 +1372,8 @@ function withAlertsFeatures() {
|
|
|
1370
1372
|
});
|
|
1371
1373
|
},
|
|
1372
1374
|
async updateAlert(alert, index) {
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
alerts.splice(index, 1, alert);
|
|
1375
|
+
if (index < 0 || index >= store.alerts().length) return;
|
|
1376
|
+
const alerts = store.alerts().with(index, alert);
|
|
1376
1377
|
await patch({ alerts }, {
|
|
1377
1378
|
type: "Alert_Update",
|
|
1378
1379
|
detail: { alert: alert.name }
|
|
@@ -1386,8 +1387,8 @@ function withAlertsFeatures() {
|
|
|
1386
1387
|
},
|
|
1387
1388
|
async updateAlerts(alerts) {
|
|
1388
1389
|
await patch({ alerts }, {
|
|
1389
|
-
type: "
|
|
1390
|
-
detail: {
|
|
1390
|
+
type: "Alerts_Update",
|
|
1391
|
+
detail: { alerts: alerts.map((a) => a.name).join(", ") }
|
|
1391
1392
|
});
|
|
1392
1393
|
patchState(store, (state) => {
|
|
1393
1394
|
return {
|
|
@@ -3243,11 +3244,13 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
3243
3244
|
});
|
|
3244
3245
|
var UserProfileService = class UserProfileService {
|
|
3245
3246
|
client = inject(ATOMIC_CLIENT);
|
|
3247
|
+
injector = inject(Injector);
|
|
3246
3248
|
resources = /* @__PURE__ */ new WeakMap();
|
|
3247
3249
|
getUserProfile(userId) {
|
|
3248
3250
|
let resourceRef = this.resources.get(userId);
|
|
3249
3251
|
if (!resourceRef) {
|
|
3250
3252
|
resourceRef = resource({
|
|
3253
|
+
injector: this.injector,
|
|
3251
3254
|
params: () => {
|
|
3252
3255
|
const id = userId();
|
|
3253
3256
|
return id ? { id } : void 0;
|
|
@@ -3426,17 +3429,61 @@ const QUERY_PARAMS_DEFAULTS = {
|
|
|
3426
3429
|
neuralSearch: void 0,
|
|
3427
3430
|
aggregation_overrides: void 0
|
|
3428
3431
|
};
|
|
3432
|
+
function parseHref(href) {
|
|
3433
|
+
let url;
|
|
3434
|
+
try {
|
|
3435
|
+
url = new URL(href, window.location.origin);
|
|
3436
|
+
} catch {
|
|
3437
|
+
return {
|
|
3438
|
+
path: "",
|
|
3439
|
+
params: new URLSearchParams()
|
|
3440
|
+
};
|
|
3441
|
+
}
|
|
3442
|
+
if (url.hash.startsWith("#/")) {
|
|
3443
|
+
const route = url.hash.slice(1);
|
|
3444
|
+
const queryIndex = route.indexOf("?");
|
|
3445
|
+
const path = queryIndex === -1 ? route : route.slice(0, queryIndex);
|
|
3446
|
+
const search = queryIndex === -1 ? "" : route.slice(queryIndex + 1);
|
|
3447
|
+
return {
|
|
3448
|
+
path,
|
|
3449
|
+
params: new URLSearchParams(search)
|
|
3450
|
+
};
|
|
3451
|
+
}
|
|
3452
|
+
return {
|
|
3453
|
+
path: "",
|
|
3454
|
+
params: url.searchParams
|
|
3455
|
+
};
|
|
3456
|
+
}
|
|
3457
|
+
function parseFilters(raw) {
|
|
3458
|
+
if (!raw) return [];
|
|
3459
|
+
const candidates = [raw];
|
|
3460
|
+
try {
|
|
3461
|
+
const decoded = decodeURIComponent(raw);
|
|
3462
|
+
if (decoded !== raw) candidates.push(decoded);
|
|
3463
|
+
} catch {}
|
|
3464
|
+
for (const candidate of candidates) try {
|
|
3465
|
+
const parsed = JSON.parse(candidate);
|
|
3466
|
+
if (Array.isArray(parsed)) return parsed;
|
|
3467
|
+
} catch {}
|
|
3468
|
+
return [];
|
|
3469
|
+
}
|
|
3429
3470
|
const QueryParamsStore = signalStore({ providedIn: "root" }, withDevtools("QueryParams"), withQueryParamsFeatures());
|
|
3430
3471
|
function withQueryParamsFeatures() {
|
|
3431
3472
|
return signalStoreFeature(withState(QUERY_PARAMS_DEFAULTS), withMethods((store, injector = inject(Injector), router = inject(Router), route = inject(ActivatedRoute), appStore = inject(AppStore)) => ({
|
|
3432
3473
|
setFromUrl(href) {
|
|
3433
3474
|
if (!href) return;
|
|
3434
|
-
const path =
|
|
3435
|
-
const
|
|
3436
|
-
const filters =
|
|
3437
|
-
const
|
|
3438
|
-
|
|
3439
|
-
|
|
3475
|
+
const { path, params } = parseHref(href);
|
|
3476
|
+
const text = params.get("q") ?? "";
|
|
3477
|
+
const filters = parseFilters(params.get("f"));
|
|
3478
|
+
const id = params.get("id") ?? "";
|
|
3479
|
+
const sort = params.get("s") ?? "";
|
|
3480
|
+
const tab = params.get("t") ?? "";
|
|
3481
|
+
const basket = params.get("b") ?? "";
|
|
3482
|
+
const name = params.get("n") ?? "";
|
|
3483
|
+
const spellingCorrectionMode = params.get("c") || void 0;
|
|
3484
|
+
const p = params.get("p");
|
|
3485
|
+
const parsedPage = p ? parseInt(p, 10) : NaN;
|
|
3486
|
+
const page = Number.isNaN(parsedPage) ? void 0 : parsedPage;
|
|
3440
3487
|
patchState(store, (state) => {
|
|
3441
3488
|
return {
|
|
3442
3489
|
...state,
|
|
@@ -4934,14 +4981,15 @@ var LoadingComponent = class LoadingComponent {
|
|
|
4934
4981
|
clearTimeout(timeout);
|
|
4935
4982
|
setTimeout(() => {
|
|
4936
4983
|
const url = this.route.snapshot.queryParams["returnUrl"] || null;
|
|
4937
|
-
if (
|
|
4938
|
-
this.router.navigateByUrl(url);
|
|
4984
|
+
if (url) {
|
|
4939
4985
|
this.queryParamsStore.setFromUrl(url);
|
|
4986
|
+
this.router.navigateByUrl(url);
|
|
4940
4987
|
} else this.router.navigate(["/"]);
|
|
4941
4988
|
}, 700);
|
|
4942
4989
|
}
|
|
4943
4990
|
});
|
|
4944
|
-
if (
|
|
4991
|
+
if (this.client.auth.isAuthenticated()) this.application.updateReadyState(true);
|
|
4992
|
+
else {
|
|
4945
4993
|
this.application.updateReadyState(false);
|
|
4946
4994
|
this.client.auth.login().then(() => {
|
|
4947
4995
|
this.application.updateReadyState(true);
|
|
@@ -5694,6 +5742,86 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
5694
5742
|
}]
|
|
5695
5743
|
}] }
|
|
5696
5744
|
});
|
|
5745
|
+
const COLOR_CLASSES = {
|
|
5746
|
+
yellow: "bg-(--bg-yellow-base-alt) text-(--font-yellow-muted)",
|
|
5747
|
+
blue: "bg-(--bg-info-base-alt) text-(--font-info-muted)",
|
|
5748
|
+
pink: "bg-(--bg-accent-base-alt) text-(--font-accent-muted)",
|
|
5749
|
+
indigo: "bg-(--bg-indigo-base-alt) text-(--font-indigo-muted)",
|
|
5750
|
+
green: "bg-(--bg-success-base-alt) text-(--font-success-muted)",
|
|
5751
|
+
orange: "bg-(--bg-warning-base-alt) text-(--font-warning-muted)"
|
|
5752
|
+
};
|
|
5753
|
+
var ListPanelEmptyComponent = class ListPanelEmptyComponent {
|
|
5754
|
+
color = input("blue", ...ngDevMode ? [{ debugName: "color" }] : /* istanbul ignore next */ []);
|
|
5755
|
+
tileClasses = computed(() => `inline-flex size-12 shrink-0 items-center justify-center rounded-(--radius-md) [&_svg]:size-6 [&_svg]:opacity-80 ${COLOR_CLASSES[this.color()]}`, ...ngDevMode ? [{ debugName: "tileClasses" }] : /* istanbul ignore next */ []);
|
|
5756
|
+
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
5757
|
+
minVersion: "12.0.0",
|
|
5758
|
+
version: "22.2.1",
|
|
5759
|
+
ngImport: i0,
|
|
5760
|
+
type: ListPanelEmptyComponent,
|
|
5761
|
+
deps: [],
|
|
5762
|
+
target: i0.ɵɵFactoryTarget.Component
|
|
5763
|
+
});
|
|
5764
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({
|
|
5765
|
+
minVersion: "17.1.0",
|
|
5766
|
+
version: "22.2.1",
|
|
5767
|
+
type: ListPanelEmptyComponent,
|
|
5768
|
+
isStandalone: true,
|
|
5769
|
+
selector: "list-panel-empty, ListPanelEmpty",
|
|
5770
|
+
inputs: { color: {
|
|
5771
|
+
classPropertyName: "color",
|
|
5772
|
+
publicName: "color",
|
|
5773
|
+
isSignal: true,
|
|
5774
|
+
isRequired: false,
|
|
5775
|
+
transformFunction: null
|
|
5776
|
+
} },
|
|
5777
|
+
host: { classAttribute: "flex flex-col items-center gap-(--spacing-xs) py-(--spacing-xs) text-center [&>[listPanelEmptyHint]]:m-0 [&>[listPanelEmptyHint]]:-mt-(--spacing-2xs) [&>[listPanelEmptyHint]]:max-w-prose [&>[listPanelEmptyHint]]:text-(length:--size-sm) [&>[listPanelEmptyHint]]:leading-(--line-height-2) [&>[listPanelEmptyHint]]:text-(--font-neutral-muted)" },
|
|
5778
|
+
ngImport: i0,
|
|
5779
|
+
template: `
|
|
5780
|
+
<span [class]="tileClasses()" aria-hidden="true">
|
|
5781
|
+
<ng-content select="[listPanelEmptyIcon]" />
|
|
5782
|
+
</span>
|
|
5783
|
+
<p class="m-0 text-(length:--size-base) leading-(--line-height-3) font-semibold text-(--font-neutral-base)">
|
|
5784
|
+
<ng-content />
|
|
5785
|
+
</p>
|
|
5786
|
+
<ng-content select="[listPanelEmptyHint]" />
|
|
5787
|
+
<ng-content select="[listPanelEmptyAction]" />
|
|
5788
|
+
`,
|
|
5789
|
+
isInline: true,
|
|
5790
|
+
changeDetection: i0.ChangeDetectionStrategy.OnPush
|
|
5791
|
+
});
|
|
5792
|
+
};
|
|
5793
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
5794
|
+
minVersion: "12.0.0",
|
|
5795
|
+
version: "22.2.1",
|
|
5796
|
+
ngImport: i0,
|
|
5797
|
+
type: ListPanelEmptyComponent,
|
|
5798
|
+
decorators: [{
|
|
5799
|
+
type: Component,
|
|
5800
|
+
args: [{
|
|
5801
|
+
selector: "list-panel-empty, ListPanelEmpty",
|
|
5802
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
5803
|
+
host: { class: "flex flex-col items-center gap-(--spacing-xs) py-(--spacing-xs) text-center [&>[listPanelEmptyHint]]:m-0 [&>[listPanelEmptyHint]]:-mt-(--spacing-2xs) [&>[listPanelEmptyHint]]:max-w-prose [&>[listPanelEmptyHint]]:text-(length:--size-sm) [&>[listPanelEmptyHint]]:leading-(--line-height-2) [&>[listPanelEmptyHint]]:text-(--font-neutral-muted)" },
|
|
5804
|
+
template: `
|
|
5805
|
+
<span [class]="tileClasses()" aria-hidden="true">
|
|
5806
|
+
<ng-content select="[listPanelEmptyIcon]" />
|
|
5807
|
+
</span>
|
|
5808
|
+
<p class="m-0 text-(length:--size-base) leading-(--line-height-3) font-semibold text-(--font-neutral-base)">
|
|
5809
|
+
<ng-content />
|
|
5810
|
+
</p>
|
|
5811
|
+
<ng-content select="[listPanelEmptyHint]" />
|
|
5812
|
+
<ng-content select="[listPanelEmptyAction]" />
|
|
5813
|
+
`
|
|
5814
|
+
}]
|
|
5815
|
+
}],
|
|
5816
|
+
propDecorators: { color: [{
|
|
5817
|
+
type: i0.Input,
|
|
5818
|
+
args: [{
|
|
5819
|
+
isSignal: true,
|
|
5820
|
+
alias: "color",
|
|
5821
|
+
required: false
|
|
5822
|
+
}]
|
|
5823
|
+
}] }
|
|
5824
|
+
});
|
|
5697
5825
|
var CollectionsDialog = class CollectionsDialog {
|
|
5698
5826
|
call = injectCallRef();
|
|
5699
5827
|
createInputElement = viewChild("createInput", ...ngDevMode ? [{ debugName: "createInputElement" }] : /* istanbul ignore next */ []);
|
|
@@ -5869,9 +5997,21 @@ var CollectionsDialog = class CollectionsDialog {
|
|
|
5869
5997
|
}
|
|
5870
5998
|
</div>
|
|
5871
5999
|
} @else {
|
|
5872
|
-
|
|
5873
|
-
|
|
5874
|
-
|
|
6000
|
+
<!-- One ListPanelEmpty per branch: an @if holding the icon and the text would be projected as a
|
|
6001
|
+
whole into the default slot, and the icon would not reach the tile. -->
|
|
6002
|
+
@if (searchText().trim()) {
|
|
6003
|
+
<ListPanelEmpty color="indigo">
|
|
6004
|
+
<search-icon listPanelEmptyIcon />
|
|
6005
|
+
{{ 'collections.noMatch' | transloco }}
|
|
6006
|
+
<p listPanelEmptyHint>{{ 'collections.noMatchHint' | transloco }}</p>
|
|
6007
|
+
</ListPanelEmpty>
|
|
6008
|
+
} @else {
|
|
6009
|
+
<ListPanelEmpty color="indigo">
|
|
6010
|
+
<inbox-icon listPanelEmptyIcon />
|
|
6011
|
+
{{ 'collections.noCollections' | transloco }}
|
|
6012
|
+
<p listPanelEmptyHint>{{ 'collections.noCollectionsDialogHint' | transloco }}</p>
|
|
6013
|
+
</ListPanelEmpty>
|
|
6014
|
+
}
|
|
5875
6015
|
}
|
|
5876
6016
|
</div>
|
|
5877
6017
|
} @else {
|
|
@@ -6008,6 +6148,24 @@ var CollectionsDialog = class CollectionsDialog {
|
|
|
6008
6148
|
],
|
|
6009
6149
|
outputs: ["checkedChange"]
|
|
6010
6150
|
},
|
|
6151
|
+
{
|
|
6152
|
+
kind: "component",
|
|
6153
|
+
type: ListPanelEmptyComponent,
|
|
6154
|
+
selector: "list-panel-empty, ListPanelEmpty",
|
|
6155
|
+
inputs: ["color"]
|
|
6156
|
+
},
|
|
6157
|
+
{
|
|
6158
|
+
kind: "component",
|
|
6159
|
+
type: InboxIcon,
|
|
6160
|
+
selector: "inbox, inbox-icon, Inbox, InboxIcon",
|
|
6161
|
+
inputs: ["class"]
|
|
6162
|
+
},
|
|
6163
|
+
{
|
|
6164
|
+
kind: "component",
|
|
6165
|
+
type: SearchIcon,
|
|
6166
|
+
selector: "search, search-icon, Search, SearchIcon",
|
|
6167
|
+
inputs: ["class"]
|
|
6168
|
+
},
|
|
6011
6169
|
{
|
|
6012
6170
|
kind: "pipe",
|
|
6013
6171
|
type: TranslocoPipe,
|
|
@@ -6037,7 +6195,10 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
6037
6195
|
DialogFooterComponent,
|
|
6038
6196
|
InputComponent,
|
|
6039
6197
|
InputControlDirective,
|
|
6040
|
-
CheckboxComponent
|
|
6198
|
+
CheckboxComponent,
|
|
6199
|
+
ListPanelEmptyComponent,
|
|
6200
|
+
InboxIcon,
|
|
6201
|
+
SearchIcon
|
|
6041
6202
|
],
|
|
6042
6203
|
providers: [provideTranslocoScope("collections")],
|
|
6043
6204
|
template: `
|
|
@@ -6086,9 +6247,21 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
6086
6247
|
}
|
|
6087
6248
|
</div>
|
|
6088
6249
|
} @else {
|
|
6089
|
-
|
|
6090
|
-
|
|
6091
|
-
|
|
6250
|
+
<!-- One ListPanelEmpty per branch: an @if holding the icon and the text would be projected as a
|
|
6251
|
+
whole into the default slot, and the icon would not reach the tile. -->
|
|
6252
|
+
@if (searchText().trim()) {
|
|
6253
|
+
<ListPanelEmpty color="indigo">
|
|
6254
|
+
<search-icon listPanelEmptyIcon />
|
|
6255
|
+
{{ 'collections.noMatch' | transloco }}
|
|
6256
|
+
<p listPanelEmptyHint>{{ 'collections.noMatchHint' | transloco }}</p>
|
|
6257
|
+
</ListPanelEmpty>
|
|
6258
|
+
} @else {
|
|
6259
|
+
<ListPanelEmpty color="indigo">
|
|
6260
|
+
<inbox-icon listPanelEmptyIcon />
|
|
6261
|
+
{{ 'collections.noCollections' | transloco }}
|
|
6262
|
+
<p listPanelEmptyHint>{{ 'collections.noCollectionsDialogHint' | transloco }}</p>
|
|
6263
|
+
</ListPanelEmpty>
|
|
6264
|
+
}
|
|
6092
6265
|
}
|
|
6093
6266
|
</div>
|
|
6094
6267
|
} @else {
|
|
@@ -7203,17 +7376,18 @@ var InfinityScrollDirective = class InfinityScrollDirective {
|
|
|
7203
7376
|
alias: "scroll-options"
|
|
7204
7377
|
});
|
|
7205
7378
|
onScroll = output();
|
|
7206
|
-
observer
|
|
7207
|
-
if (entry.isIntersecting) this.onScroll.emit();
|
|
7208
|
-
}, this.options());
|
|
7379
|
+
observer;
|
|
7209
7380
|
el = inject(ElementRef);
|
|
7210
7381
|
constructor() {
|
|
7211
7382
|
afterNextRender(() => {
|
|
7383
|
+
this.observer = new IntersectionObserver(([entry]) => {
|
|
7384
|
+
if (entry.isIntersecting) this.onScroll.emit();
|
|
7385
|
+
}, this.options());
|
|
7212
7386
|
this.observer.observe(this.el.nativeElement);
|
|
7213
7387
|
});
|
|
7214
7388
|
}
|
|
7215
7389
|
ngOnDestroy() {
|
|
7216
|
-
this.observer
|
|
7390
|
+
this.observer?.disconnect();
|
|
7217
7391
|
}
|
|
7218
7392
|
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
7219
7393
|
minVersion: "12.0.0",
|
|
@@ -16044,6 +16218,70 @@ var Alert;
|
|
|
16044
16218
|
})(Alert.Days || (Alert.Days = {}));
|
|
16045
16219
|
})(Alert || (Alert = {}));
|
|
16046
16220
|
const COMPONENTS_FOR_DOCUMENT_TYPE = new InjectionToken("COMPONENTS_FOR_DOCUMENT_TYPE");
|
|
16221
|
+
const WEEKDAY_FLAGS = [
|
|
16222
|
+
Alert.Days.Monday,
|
|
16223
|
+
Alert.Days.Tuesday,
|
|
16224
|
+
Alert.Days.Wednesday,
|
|
16225
|
+
Alert.Days.Thursday,
|
|
16226
|
+
Alert.Days.Friday,
|
|
16227
|
+
Alert.Days.Saturday,
|
|
16228
|
+
Alert.Days.Sunday
|
|
16229
|
+
];
|
|
16230
|
+
const TIMED_FREQUENCIES = [
|
|
16231
|
+
Alert.Frequency.Daily,
|
|
16232
|
+
Alert.Frequency.Weekly,
|
|
16233
|
+
Alert.Frequency.Monthly
|
|
16234
|
+
];
|
|
16235
|
+
function weekdayNames(lang) {
|
|
16236
|
+
const format = new Intl.DateTimeFormat(lang, { weekday: "short" });
|
|
16237
|
+
return WEEKDAY_FLAGS.map((_, offset) => format.format(new Date(2024, 0, 1 + offset)));
|
|
16238
|
+
}
|
|
16239
|
+
function formatDays(days, lang) {
|
|
16240
|
+
const names = weekdayNames(lang);
|
|
16241
|
+
const selected = WEEKDAY_FLAGS.map((flag) => (days & flag) !== 0);
|
|
16242
|
+
const parts = [];
|
|
16243
|
+
let start = 0;
|
|
16244
|
+
while (start < selected.length) {
|
|
16245
|
+
if (!selected[start]) {
|
|
16246
|
+
start++;
|
|
16247
|
+
continue;
|
|
16248
|
+
}
|
|
16249
|
+
let end = start;
|
|
16250
|
+
while (end + 1 < selected.length && selected[end + 1]) end++;
|
|
16251
|
+
if (end - start >= 2) parts.push(`${names[start]}–${names[end]}`);
|
|
16252
|
+
else for (let i = start; i <= end; i++) parts.push(names[i]);
|
|
16253
|
+
start = end + 1;
|
|
16254
|
+
}
|
|
16255
|
+
return parts.join(", ");
|
|
16256
|
+
}
|
|
16257
|
+
function alertSummary(lang, alert) {
|
|
16258
|
+
const parts = [];
|
|
16259
|
+
if (!alert.active) parts.push({ key: "alerts.paused" });
|
|
16260
|
+
if (alert.frequency !== void 0 && Alert.Frequency[alert.frequency] !== void 0) parts.push({ key: `alerts.frequency.${Alert.Frequency[alert.frequency]}` });
|
|
16261
|
+
if (alert.times && TIMED_FREQUENCIES.includes(alert.frequency)) parts.push({ text: alert.times });
|
|
16262
|
+
const days = alert.days ?? Alert.Days.None;
|
|
16263
|
+
if (alert.frequency !== Alert.Frequency.Immediate && days !== Alert.Days.None && (days & Alert.Days.AllDays) !== Alert.Days.AllDays) parts.push({ text: formatDays(days, lang) });
|
|
16264
|
+
return parts;
|
|
16265
|
+
}
|
|
16266
|
+
const ALREADY_NOTIFIED = [
|
|
16267
|
+
400,
|
|
16268
|
+
403,
|
|
16269
|
+
500,
|
|
16270
|
+
503
|
|
16271
|
+
];
|
|
16272
|
+
function saveErrorDetail(error) {
|
|
16273
|
+
const { errorMessage, message } = error ?? {};
|
|
16274
|
+
return errorMessage ?? message;
|
|
16275
|
+
}
|
|
16276
|
+
function notifySaveFailed(transloco, error) {
|
|
16277
|
+
const { status } = error ?? {};
|
|
16278
|
+
if (status !== void 0 && ALREADY_NOTIFIED.includes(status)) return;
|
|
16279
|
+
notify.error(transloco.translate("alerts.saveFailed"), {
|
|
16280
|
+
description: saveErrorDetail(error),
|
|
16281
|
+
closeButton: true,
|
|
16282
|
+
duration: 5e3
|
|
16283
|
+
});
|
|
16284
|
+
}
|
|
16047
16285
|
const WEEKDAYS = [
|
|
16048
16286
|
{
|
|
16049
16287
|
key: "monday",
|
|
@@ -16118,6 +16356,7 @@ var AlertDialog = class AlertDialog {
|
|
|
16118
16356
|
alert = signal(void 0, ...ngDevMode ? [{ debugName: "alert" }] : /* istanbul ignore next */ []);
|
|
16119
16357
|
canUpdateQuery = signal(false, ...ngDevMode ? [{ debugName: "canUpdateQuery" }] : /* istanbul ignore next */ []);
|
|
16120
16358
|
updateStatus = signal("idle", ...ngDevMode ? [{ debugName: "updateStatus" }] : /* istanbul ignore next */ []);
|
|
16359
|
+
saveError = signal(void 0, ...ngDevMode ? [{ debugName: "saveError" }] : /* istanbul ignore next */ []);
|
|
16121
16360
|
alertModel = signal(emptyAlertModel(), ...ngDevMode ? [{ debugName: "alertModel" }] : /* istanbul ignore next */ []);
|
|
16122
16361
|
alertForm = form(this.alertModel, alertSchema, {
|
|
16123
16362
|
name: "alert",
|
|
@@ -16146,8 +16385,14 @@ var AlertDialog = class AlertDialog {
|
|
|
16146
16385
|
this.dialog()?.open();
|
|
16147
16386
|
}
|
|
16148
16387
|
async persist() {
|
|
16149
|
-
|
|
16150
|
-
|
|
16388
|
+
this.saveError.set(void 0);
|
|
16389
|
+
try {
|
|
16390
|
+
if (this.alert()) await this.update();
|
|
16391
|
+
else await this.create();
|
|
16392
|
+
} catch (error) {
|
|
16393
|
+
this.saveError.set({ detail: saveErrorDetail(error) });
|
|
16394
|
+
return;
|
|
16395
|
+
}
|
|
16151
16396
|
this.dialog()?.close("dialog-confirm");
|
|
16152
16397
|
}
|
|
16153
16398
|
async create() {
|
|
@@ -16166,19 +16411,21 @@ var AlertDialog = class AlertDialog {
|
|
|
16166
16411
|
combine: true,
|
|
16167
16412
|
respectTabSelection: false
|
|
16168
16413
|
};
|
|
16169
|
-
this.userSettingsStore.createAlert(alert);
|
|
16414
|
+
await this.userSettingsStore.createAlert(alert);
|
|
16170
16415
|
}
|
|
16171
16416
|
async update() {
|
|
16172
16417
|
const alert = this.alert();
|
|
16173
16418
|
const index = this.index();
|
|
16174
16419
|
if (!alert || index === void 0) return;
|
|
16175
16420
|
const model = this.alertModel();
|
|
16176
|
-
|
|
16177
|
-
|
|
16178
|
-
|
|
16179
|
-
|
|
16180
|
-
|
|
16181
|
-
|
|
16421
|
+
await this.userSettingsStore.updateAlert({
|
|
16422
|
+
...alert,
|
|
16423
|
+
name: model.name.trim(),
|
|
16424
|
+
frequency: model.frequency ?? Alert.Frequency.Daily,
|
|
16425
|
+
days: flagsFromWeekdays(model.days),
|
|
16426
|
+
times: model.time,
|
|
16427
|
+
active: model.active
|
|
16428
|
+
}, index);
|
|
16182
16429
|
}
|
|
16183
16430
|
showError(field) {
|
|
16184
16431
|
return fieldShowsError(field);
|
|
@@ -16190,9 +16437,12 @@ var AlertDialog = class AlertDialog {
|
|
|
16190
16437
|
const alert = this.alert();
|
|
16191
16438
|
const index = this.index();
|
|
16192
16439
|
if (!alert || index === void 0) return;
|
|
16440
|
+
this.saveError.set(void 0);
|
|
16193
16441
|
this.updateStatus.set("updating");
|
|
16194
|
-
|
|
16195
|
-
|
|
16442
|
+
this.userSettingsStore.updateAlert({
|
|
16443
|
+
...alert,
|
|
16444
|
+
query: this.queryParamsStore.getQuery()
|
|
16445
|
+
}, index).then(() => {
|
|
16196
16446
|
setTimeout(() => {
|
|
16197
16447
|
this.updateStatus.set("updated");
|
|
16198
16448
|
}, 500);
|
|
@@ -16200,6 +16450,9 @@ var AlertDialog = class AlertDialog {
|
|
|
16200
16450
|
setTimeout(() => {
|
|
16201
16451
|
this.updateStatus.set("idle");
|
|
16202
16452
|
}, 2e3);
|
|
16453
|
+
}).catch((error) => {
|
|
16454
|
+
this.updateStatus.set("idle");
|
|
16455
|
+
this.saveError.set({ detail: saveErrorDetail(error) });
|
|
16203
16456
|
});
|
|
16204
16457
|
}
|
|
16205
16458
|
execute() {
|
|
@@ -16352,6 +16605,25 @@ var AlertDialog = class AlertDialog {
|
|
|
16352
16605
|
</DialogBody>
|
|
16353
16606
|
|
|
16354
16607
|
<DialogFooter class="flex-col">
|
|
16608
|
+
<!-- Shown here and not as a toast: the dialog is modal, so it is in the top layer, above any toast. -->
|
|
16609
|
+
@if (saveError(); as failure) {
|
|
16610
|
+
<div class="save-error w-full">
|
|
16611
|
+
<div class="min-h-0 overflow-hidden">
|
|
16612
|
+
<div
|
|
16613
|
+
id="alertSaveError"
|
|
16614
|
+
role="alert"
|
|
16615
|
+
class="flex w-full items-start gap-(--spacing-sm) rounded-(--radius-md) border border-(--stroke-error-base) bg-(--bg-error-lightest) p-(--spacing-md) text-(--font-error-base)">
|
|
16616
|
+
<TriangleAlert class="mt-0.5 size-4 shrink-0" width="16" height="16" aria-hidden="true" />
|
|
16617
|
+
<div class="flex min-w-0 flex-col gap-(--spacing-2xs)">
|
|
16618
|
+
<p class="m-0 font-medium">{{ 'alerts.saveFailed' | transloco }}</p>
|
|
16619
|
+
@if (failure.detail) {
|
|
16620
|
+
<p class="m-0 text-(length:--size-sm) wrap-anywhere opacity-80">{{ failure.detail }}</p>
|
|
16621
|
+
}
|
|
16622
|
+
</div>
|
|
16623
|
+
</div>
|
|
16624
|
+
</div>
|
|
16625
|
+
</div>
|
|
16626
|
+
}
|
|
16355
16627
|
@if (alert() || canUpdateQuery()) {
|
|
16356
16628
|
<div class="flex w-full flex-col gap-2">
|
|
16357
16629
|
@if (alert()) {
|
|
@@ -16398,7 +16670,7 @@ var AlertDialog = class AlertDialog {
|
|
|
16398
16670
|
</dialog>
|
|
16399
16671
|
`,
|
|
16400
16672
|
isInline: true,
|
|
16401
|
-
styles: [".weekdays-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(100px,1fr))}\n"],
|
|
16673
|
+
styles: [".weekdays-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(100px,1fr))}.save-error{display:grid;animation:save-error-in .22s ease-out}.save-error>div>div{animation:save-error-card-in .22s ease-out}@keyframes save-error-in{0%{grid-template-rows:0fr}to{grid-template-rows:1fr}}@keyframes save-error-card-in{0%{opacity:0;transform:translateY(-6px)}to{opacity:1;transform:none}}@media(prefers-reduced-motion:reduce){.save-error,.save-error>div>div{animation:none}}\n"],
|
|
16402
16674
|
dependencies: [
|
|
16403
16675
|
{
|
|
16404
16676
|
kind: "directive",
|
|
@@ -16558,6 +16830,12 @@ var AlertDialog = class AlertDialog {
|
|
|
16558
16830
|
selector: "circle-check, circle-check-icon, CircleCheck, CircleCheckIcon",
|
|
16559
16831
|
inputs: ["class"]
|
|
16560
16832
|
},
|
|
16833
|
+
{
|
|
16834
|
+
kind: "component",
|
|
16835
|
+
type: TriangleAlertIcon,
|
|
16836
|
+
selector: "triangle-alert, triangle-alert-icon, TriangleAlert, TriangleAlertIcon",
|
|
16837
|
+
inputs: ["class"]
|
|
16838
|
+
},
|
|
16561
16839
|
{
|
|
16562
16840
|
kind: "pipe",
|
|
16563
16841
|
type: TranslocoPipe,
|
|
@@ -16594,7 +16872,8 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
16594
16872
|
ChevronDownIcon,
|
|
16595
16873
|
CheckIcon,
|
|
16596
16874
|
LoadingCircleIcon,
|
|
16597
|
-
CircleCheckIcon
|
|
16875
|
+
CircleCheckIcon,
|
|
16876
|
+
TriangleAlertIcon
|
|
16598
16877
|
],
|
|
16599
16878
|
providers: [provideTranslocoScope("alerts")],
|
|
16600
16879
|
template: `
|
|
@@ -16709,6 +16988,25 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
16709
16988
|
</DialogBody>
|
|
16710
16989
|
|
|
16711
16990
|
<DialogFooter class="flex-col">
|
|
16991
|
+
<!-- Shown here and not as a toast: the dialog is modal, so it is in the top layer, above any toast. -->
|
|
16992
|
+
@if (saveError(); as failure) {
|
|
16993
|
+
<div class="save-error w-full">
|
|
16994
|
+
<div class="min-h-0 overflow-hidden">
|
|
16995
|
+
<div
|
|
16996
|
+
id="alertSaveError"
|
|
16997
|
+
role="alert"
|
|
16998
|
+
class="flex w-full items-start gap-(--spacing-sm) rounded-(--radius-md) border border-(--stroke-error-base) bg-(--bg-error-lightest) p-(--spacing-md) text-(--font-error-base)">
|
|
16999
|
+
<TriangleAlert class="mt-0.5 size-4 shrink-0" width="16" height="16" aria-hidden="true" />
|
|
17000
|
+
<div class="flex min-w-0 flex-col gap-(--spacing-2xs)">
|
|
17001
|
+
<p class="m-0 font-medium">{{ 'alerts.saveFailed' | transloco }}</p>
|
|
17002
|
+
@if (failure.detail) {
|
|
17003
|
+
<p class="m-0 text-(length:--size-sm) wrap-anywhere opacity-80">{{ failure.detail }}</p>
|
|
17004
|
+
}
|
|
17005
|
+
</div>
|
|
17006
|
+
</div>
|
|
17007
|
+
</div>
|
|
17008
|
+
</div>
|
|
17009
|
+
}
|
|
16712
17010
|
@if (alert() || canUpdateQuery()) {
|
|
16713
17011
|
<div class="flex w-full flex-col gap-2">
|
|
16714
17012
|
@if (alert()) {
|
|
@@ -16754,7 +17052,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
16754
17052
|
</DialogContent>
|
|
16755
17053
|
</dialog>
|
|
16756
17054
|
`,
|
|
16757
|
-
styles: [".weekdays-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(100px,1fr))}\n"]
|
|
17055
|
+
styles: [".weekdays-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(100px,1fr))}.save-error{display:grid;animation:save-error-in .22s ease-out}.save-error>div>div{animation:save-error-card-in .22s ease-out}@keyframes save-error-in{0%{grid-template-rows:0fr}to{grid-template-rows:1fr}}@keyframes save-error-card-in{0%{opacity:0;transform:translateY(-6px)}to{opacity:1;transform:none}}@media(prefers-reduced-motion:reduce){.save-error,.save-error>div>div{animation:none}}\n"]
|
|
16758
17056
|
}]
|
|
16759
17057
|
}],
|
|
16760
17058
|
ctorParameters: () => [],
|
|
@@ -16764,6 +17062,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
16764
17062
|
}] }
|
|
16765
17063
|
});
|
|
16766
17064
|
const AlertForm = createCallable(AlertDialog);
|
|
17065
|
+
const UNDO_DELAY = 6e3;
|
|
16767
17066
|
var AlertsComponent = class AlertsComponent {
|
|
16768
17067
|
floating = inject(PopoverDirective, {
|
|
16769
17068
|
skipSelf: true,
|
|
@@ -16771,17 +17070,42 @@ var AlertsComponent = class AlertsComponent {
|
|
|
16771
17070
|
});
|
|
16772
17071
|
userSettingsStore = inject(UserSettingsStore);
|
|
16773
17072
|
injector = inject(Injector);
|
|
16774
|
-
|
|
16775
|
-
|
|
16776
|
-
tmpAlerts = [];
|
|
17073
|
+
transloco = inject(TranslocoService);
|
|
17074
|
+
lang = toSignal(this.transloco.langChanges$);
|
|
16777
17075
|
query;
|
|
17076
|
+
alerts = computed(() => this.userSettingsStore.alerts() ?? [], ...ngDevMode ? [{ debugName: "alerts" }] : /* istanbul ignore next */ []);
|
|
17077
|
+
order = linkedSignal(() => [...this.alerts()], ...ngDevMode ? [{ debugName: "order" }] : /* istanbul ignore next */ []);
|
|
17078
|
+
hidden = signal(/* @__PURE__ */ new Set(), ...ngDevMode ? [{ debugName: "hidden" }] : /* istanbul ignore next */ []);
|
|
17079
|
+
pendingDeletion = signal(void 0, ...ngDevMode ? [{ debugName: "pendingDeletion" }] : /* istanbul ignore next */ []);
|
|
17080
|
+
undoTimer;
|
|
17081
|
+
visible = computed(() => {
|
|
17082
|
+
const hidden = this.hidden();
|
|
17083
|
+
return this.order().filter((alert) => !hidden.has(alert));
|
|
17084
|
+
}, ...ngDevMode ? [{ debugName: "visible" }] : /* istanbul ignore next */ []);
|
|
17085
|
+
summaries = computed(() => {
|
|
17086
|
+
const lang = this.lang() ?? this.transloco.getActiveLang();
|
|
17087
|
+
return new Map(this.order().map((alert) => [alert, alertSummary(lang, alert)]));
|
|
17088
|
+
}, ...ngDevMode ? [{ debugName: "summaries" }] : /* istanbul ignore next */ []);
|
|
17089
|
+
summaryOf = (alert) => this.summaries().get(alert) ?? [];
|
|
17090
|
+
announceMove = (alert, position, total) => this.transloco.translate("alerts.moved", {
|
|
17091
|
+
name: alert.name,
|
|
17092
|
+
position,
|
|
17093
|
+
total
|
|
17094
|
+
});
|
|
16778
17095
|
constructor() {
|
|
16779
17096
|
this.query = buildQuery();
|
|
16780
|
-
|
|
16781
|
-
|
|
16782
|
-
|
|
17097
|
+
inject(DestroyRef).onDestroy(() => this.commitDeletion());
|
|
17098
|
+
}
|
|
17099
|
+
onRowClick(event, alert) {
|
|
17100
|
+
if (event.target.closest("[data-row-action]")) return;
|
|
17101
|
+
this.edit(alert);
|
|
16783
17102
|
}
|
|
16784
|
-
|
|
17103
|
+
onRowEnter(event, alert) {
|
|
17104
|
+
if (event.target === event.currentTarget) this.edit(alert);
|
|
17105
|
+
}
|
|
17106
|
+
edit(alert) {
|
|
17107
|
+
const index = this.alerts().indexOf(alert);
|
|
17108
|
+
if (index < 0) return;
|
|
16785
17109
|
this.floating?.close();
|
|
16786
17110
|
AlertForm.call(index, { injector: this.injector });
|
|
16787
17111
|
}
|
|
@@ -16789,19 +17113,73 @@ var AlertsComponent = class AlertsComponent {
|
|
|
16789
17113
|
this.floating?.close();
|
|
16790
17114
|
AlertForm.call(void 0, { injector: this.injector });
|
|
16791
17115
|
}
|
|
16792
|
-
|
|
16793
|
-
|
|
16794
|
-
this.
|
|
17116
|
+
pendingActive = signal(/* @__PURE__ */ new Map(), ...ngDevMode ? [{ debugName: "pendingActive" }] : /* istanbul ignore next */ []);
|
|
17117
|
+
isActive(alert) {
|
|
17118
|
+
return this.pendingActive().get(alert) ?? alert.active;
|
|
17119
|
+
}
|
|
17120
|
+
async toggle(alert, active) {
|
|
17121
|
+
const index = this.alerts().indexOf(alert);
|
|
17122
|
+
if (index < 0) return;
|
|
17123
|
+
this.pendingActive.update((pending) => new Map(pending).set(alert, active));
|
|
17124
|
+
try {
|
|
17125
|
+
await this.userSettingsStore.updateAlert({
|
|
17126
|
+
...alert,
|
|
17127
|
+
active
|
|
17128
|
+
}, index);
|
|
17129
|
+
} catch (error) {
|
|
17130
|
+
notifySaveFailed(this.transloco, error);
|
|
17131
|
+
} finally {
|
|
17132
|
+
this.pendingActive.update((pending) => {
|
|
17133
|
+
const next = new Map(pending);
|
|
17134
|
+
next.delete(alert);
|
|
17135
|
+
return next;
|
|
17136
|
+
});
|
|
17137
|
+
}
|
|
16795
17138
|
}
|
|
16796
|
-
|
|
16797
|
-
|
|
16798
|
-
|
|
16799
|
-
|
|
16800
|
-
|
|
17139
|
+
deleteAlert(alert) {
|
|
17140
|
+
this.commitDeletion();
|
|
17141
|
+
this.hidden.update((hidden) => new Set(hidden).add(alert));
|
|
17142
|
+
this.pendingDeletion.set(alert);
|
|
17143
|
+
this.undoTimer = setTimeout(() => this.commitDeletion(), UNDO_DELAY);
|
|
16801
17144
|
}
|
|
16802
|
-
|
|
16803
|
-
|
|
16804
|
-
|
|
17145
|
+
undoDeletion() {
|
|
17146
|
+
clearTimeout(this.undoTimer);
|
|
17147
|
+
const alert = this.pendingDeletion();
|
|
17148
|
+
if (!alert) return;
|
|
17149
|
+
this.pendingDeletion.set(void 0);
|
|
17150
|
+
this.unhide(alert);
|
|
17151
|
+
}
|
|
17152
|
+
commitDeletion() {
|
|
17153
|
+
clearTimeout(this.undoTimer);
|
|
17154
|
+
const alert = this.pendingDeletion();
|
|
17155
|
+
if (!alert) return;
|
|
17156
|
+
this.pendingDeletion.set(void 0);
|
|
17157
|
+
const index = this.alerts().indexOf(alert);
|
|
17158
|
+
if (index < 0) {
|
|
17159
|
+
this.unhide(alert);
|
|
17160
|
+
return;
|
|
17161
|
+
}
|
|
17162
|
+
this.userSettingsStore.deleteAlert(index).catch((error) => notifySaveFailed(this.transloco, error)).finally(() => this.unhide(alert));
|
|
17163
|
+
}
|
|
17164
|
+
unhide(alert) {
|
|
17165
|
+
this.hidden.update((hidden) => {
|
|
17166
|
+
const next = new Set(hidden);
|
|
17167
|
+
next.delete(alert);
|
|
17168
|
+
return next;
|
|
17169
|
+
});
|
|
17170
|
+
}
|
|
17171
|
+
async saveOrder(shown) {
|
|
17172
|
+
const hidden = this.hidden();
|
|
17173
|
+
const previous = this.order();
|
|
17174
|
+
const next = [...shown, ...previous.filter((alert) => hidden.has(alert))];
|
|
17175
|
+
if (next.every((alert, index) => alert === previous[index])) return;
|
|
17176
|
+
this.order.set(next);
|
|
17177
|
+
try {
|
|
17178
|
+
await this.userSettingsStore.updateAlerts(next);
|
|
17179
|
+
} catch (error) {
|
|
17180
|
+
this.order.set(previous);
|
|
17181
|
+
notifySaveFailed(this.transloco, error);
|
|
17182
|
+
}
|
|
16805
17183
|
}
|
|
16806
17184
|
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
16807
17185
|
minVersion: "12.0.0",
|
|
@@ -16819,7 +17197,7 @@ var AlertsComponent = class AlertsComponent {
|
|
|
16819
17197
|
selector: "Alerts",
|
|
16820
17198
|
providers: [provideTranslocoScope("alerts")],
|
|
16821
17199
|
ngImport: i0,
|
|
16822
|
-
template: "<
|
|
17200
|
+
template: "<div class=\"flex min-w-80 flex-col gap-(--spacing-3xs) p-2\">\n <div class=\"flex items-center gap-(--spacing-2xs) ps-(--spacing-2xs)\">\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 <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 flex min-h-12 w-full cursor-pointer items-center gap-(--spacing-3xs) rounded-(--radius-sm) py-(--spacing-3xs) ps-(--spacing-3xs) pe-(--spacing-2xs) 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 <!-- 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 <span class=\"flex min-w-0 flex-1 flex-col\">\n <span\n class=\"line-clamp-1 text-(length:--size-base) group-data-inactive:text-(--font-neutral-muted)\"\n >{{ name }}</span\n >\n @if (summaryOf(alert).length) {\n <span\n class=\"line-clamp-1 text-(length:--size-sm) text-(--font-neutral-muted)\">\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 }\n </span>\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:opacity-100\"\n [attr.title]=\"'alerts.deleteAlertNamed' | transloco: { name }\"\n [attr.aria-label]=\"'alerts.deleteAlertNamed' | transloco: { name }\"\n (click)=\"deleteAlert(alert)\">\n <TrashIcon />\n </button>\n </span>\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 @if (visible().length > 1) {\n <p\n class=\"m-0 ps-(--spacing-2xs) pt-(--spacing-3xs) text-(length:--size-sm) text-(--font-neutral-muted)\">\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",
|
|
16823
17201
|
dependencies: [
|
|
16824
17202
|
{
|
|
16825
17203
|
kind: "directive",
|
|
@@ -16834,63 +17212,39 @@ var AlertsComponent = class AlertsComponent {
|
|
|
16834
17212
|
]
|
|
16835
17213
|
},
|
|
16836
17214
|
{
|
|
16837
|
-
kind: "
|
|
16838
|
-
type:
|
|
17215
|
+
kind: "component",
|
|
17216
|
+
type: SwitchComponent,
|
|
17217
|
+
selector: "switch, Switch",
|
|
17218
|
+
inputs: [
|
|
17219
|
+
"checked",
|
|
17220
|
+
"disabled",
|
|
17221
|
+
"size",
|
|
17222
|
+
"class",
|
|
17223
|
+
"aria-label",
|
|
17224
|
+
"aria-labelledby",
|
|
17225
|
+
"presentational",
|
|
17226
|
+
"compact",
|
|
17227
|
+
"tabIndex"
|
|
17228
|
+
],
|
|
17229
|
+
outputs: ["checkedChange"]
|
|
16839
17230
|
},
|
|
16840
17231
|
{
|
|
16841
17232
|
kind: "directive",
|
|
16842
|
-
type:
|
|
16843
|
-
selector: "[
|
|
17233
|
+
type: SortableDirective,
|
|
17234
|
+
selector: "[sortable]",
|
|
16844
17235
|
inputs: [
|
|
16845
|
-
"
|
|
16846
|
-
"
|
|
16847
|
-
"
|
|
16848
|
-
"id",
|
|
16849
|
-
"cdkDropListLockAxis",
|
|
16850
|
-
"cdkDropListDisabled",
|
|
16851
|
-
"cdkDropListSortingDisabled",
|
|
16852
|
-
"cdkDropListEnterPredicate",
|
|
16853
|
-
"cdkDropListSortPredicate",
|
|
16854
|
-
"cdkDropListAutoScrollDisabled",
|
|
16855
|
-
"cdkDropListAutoScrollStep",
|
|
16856
|
-
"cdkDropListElementContainer",
|
|
16857
|
-
"cdkDropListHasAnchor"
|
|
17236
|
+
"sortable",
|
|
17237
|
+
"sortableDisabled",
|
|
17238
|
+
"sortableAnnounce"
|
|
16858
17239
|
],
|
|
16859
|
-
outputs: [
|
|
16860
|
-
|
|
16861
|
-
"cdkDropListEntered",
|
|
16862
|
-
"cdkDropListExited",
|
|
16863
|
-
"cdkDropListSorted"
|
|
16864
|
-
],
|
|
16865
|
-
exportAs: ["cdkDropList"]
|
|
17240
|
+
outputs: ["reorder"],
|
|
17241
|
+
exportAs: ["sortable"]
|
|
16866
17242
|
},
|
|
16867
17243
|
{
|
|
16868
17244
|
kind: "directive",
|
|
16869
|
-
type:
|
|
16870
|
-
selector: "[
|
|
16871
|
-
inputs: [
|
|
16872
|
-
"cdkDragData",
|
|
16873
|
-
"cdkDragLockAxis",
|
|
16874
|
-
"cdkDragRootElement",
|
|
16875
|
-
"cdkDragBoundary",
|
|
16876
|
-
"cdkDragStartDelay",
|
|
16877
|
-
"cdkDragFreeDragPosition",
|
|
16878
|
-
"cdkDragDisabled",
|
|
16879
|
-
"cdkDragConstrainPosition",
|
|
16880
|
-
"cdkDragPreviewClass",
|
|
16881
|
-
"cdkDragPreviewContainer",
|
|
16882
|
-
"cdkDragScale"
|
|
16883
|
-
],
|
|
16884
|
-
outputs: [
|
|
16885
|
-
"cdkDragStarted",
|
|
16886
|
-
"cdkDragReleased",
|
|
16887
|
-
"cdkDragEnded",
|
|
16888
|
-
"cdkDragEntered",
|
|
16889
|
-
"cdkDragExited",
|
|
16890
|
-
"cdkDragDropped",
|
|
16891
|
-
"cdkDragMoved"
|
|
16892
|
-
],
|
|
16893
|
-
exportAs: ["cdkDrag"]
|
|
17245
|
+
type: SortableItemDirective,
|
|
17246
|
+
selector: "[sortableItem]",
|
|
17247
|
+
inputs: ["sortableItem", "sortableItemDisabled"]
|
|
16894
17248
|
},
|
|
16895
17249
|
{
|
|
16896
17250
|
kind: "component",
|
|
@@ -16898,6 +17252,24 @@ var AlertsComponent = class AlertsComponent {
|
|
|
16898
17252
|
selector: "bell, bell-icon, Bell, BellIcon",
|
|
16899
17253
|
inputs: ["class"]
|
|
16900
17254
|
},
|
|
17255
|
+
{
|
|
17256
|
+
kind: "component",
|
|
17257
|
+
type: BellSlashIcon,
|
|
17258
|
+
selector: "bell-slash, bell-slash-icon, BellSlash, BellSlashIcon",
|
|
17259
|
+
inputs: ["class"]
|
|
17260
|
+
},
|
|
17261
|
+
{
|
|
17262
|
+
kind: "component",
|
|
17263
|
+
type: GripVerticalIcon,
|
|
17264
|
+
selector: "grip-vertical, grip-vertical-icon, GripVertical, GripVerticalIcon",
|
|
17265
|
+
inputs: ["class"]
|
|
17266
|
+
},
|
|
17267
|
+
{
|
|
17268
|
+
kind: "component",
|
|
17269
|
+
type: PlusIcon,
|
|
17270
|
+
selector: "plus, plus-icon, Plus, PlusIcon",
|
|
17271
|
+
inputs: ["class"]
|
|
17272
|
+
},
|
|
16901
17273
|
{
|
|
16902
17274
|
kind: "component",
|
|
16903
17275
|
type: TrashIcon,
|
|
@@ -16906,9 +17278,9 @@ var AlertsComponent = class AlertsComponent {
|
|
|
16906
17278
|
},
|
|
16907
17279
|
{
|
|
16908
17280
|
kind: "component",
|
|
16909
|
-
type:
|
|
16910
|
-
selector: "
|
|
16911
|
-
inputs: ["
|
|
17281
|
+
type: ListPanelEmptyComponent,
|
|
17282
|
+
selector: "list-panel-empty, ListPanelEmpty",
|
|
17283
|
+
inputs: ["color"]
|
|
16912
17284
|
},
|
|
16913
17285
|
{
|
|
16914
17286
|
kind: "pipe",
|
|
@@ -16930,13 +17302,18 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
16930
17302
|
imports: [
|
|
16931
17303
|
TranslocoPipe,
|
|
16932
17304
|
ButtonComponent,
|
|
16933
|
-
|
|
17305
|
+
SwitchComponent,
|
|
17306
|
+
SortableDirective,
|
|
17307
|
+
SortableItemDirective,
|
|
16934
17308
|
BellIcon,
|
|
17309
|
+
BellSlashIcon,
|
|
17310
|
+
GripVerticalIcon,
|
|
17311
|
+
PlusIcon,
|
|
16935
17312
|
TrashIcon,
|
|
16936
|
-
|
|
17313
|
+
ListPanelEmptyComponent
|
|
16937
17314
|
],
|
|
16938
17315
|
providers: [provideTranslocoScope("alerts")],
|
|
16939
|
-
template: "<
|
|
17316
|
+
template: "<div class=\"flex min-w-80 flex-col gap-(--spacing-3xs) p-2\">\n <div class=\"flex items-center gap-(--spacing-2xs) ps-(--spacing-2xs)\">\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 <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 flex min-h-12 w-full cursor-pointer items-center gap-(--spacing-3xs) rounded-(--radius-sm) py-(--spacing-3xs) ps-(--spacing-3xs) pe-(--spacing-2xs) 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 <!-- 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 <span class=\"flex min-w-0 flex-1 flex-col\">\n <span\n class=\"line-clamp-1 text-(length:--size-base) group-data-inactive:text-(--font-neutral-muted)\"\n >{{ name }}</span\n >\n @if (summaryOf(alert).length) {\n <span\n class=\"line-clamp-1 text-(length:--size-sm) text-(--font-neutral-muted)\">\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 }\n </span>\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:opacity-100\"\n [attr.title]=\"'alerts.deleteAlertNamed' | transloco: { name }\"\n [attr.aria-label]=\"'alerts.deleteAlertNamed' | transloco: { name }\"\n (click)=\"deleteAlert(alert)\">\n <TrashIcon />\n </button>\n </span>\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 @if (visible().length > 1) {\n <p\n class=\"m-0 ps-(--spacing-2xs) pt-(--spacing-3xs) text-(length:--size-sm) text-(--font-neutral-muted)\">\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"
|
|
16940
17317
|
}]
|
|
16941
17318
|
}],
|
|
16942
17319
|
ctorParameters: () => []
|
|
@@ -18692,7 +19069,6 @@ var BookmarkButtonComponent = class BookmarkButtonComponent {
|
|
|
18692
19069
|
isBookmarked = computed(() => {
|
|
18693
19070
|
return this.userSettingsStore.isBookmarked(this.article());
|
|
18694
19071
|
}, ...ngDevMode ? [{ debugName: "isBookmarked" }] : /* istanbul ignore next */ []);
|
|
18695
|
-
title = computed(() => this.transloco.translate("bookmarks.bookmarkDocument"), ...ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []);
|
|
18696
19072
|
async bookmark(e) {
|
|
18697
19073
|
e.stopPropagation();
|
|
18698
19074
|
const article = this.article();
|
|
@@ -18756,11 +19132,11 @@ var BookmarkButtonComponent = class BookmarkButtonComponent {
|
|
|
18756
19132
|
properties: { "class.visible": "isBookmarked()" },
|
|
18757
19133
|
classAttribute: "cursor-pointer"
|
|
18758
19134
|
},
|
|
18759
|
-
providers: [provideTranslocoScope("
|
|
19135
|
+
providers: [provideTranslocoScope("bookmarks")],
|
|
18760
19136
|
ngImport: i0,
|
|
18761
19137
|
template: `
|
|
18762
19138
|
<button [variant]="variant()" [size]="size()" [iconOnly]="true"
|
|
18763
|
-
[tooltip]="
|
|
19139
|
+
[tooltip]="'bookmarks.bookmarkDocument' | transloco"
|
|
18764
19140
|
[attr.aria-pressed]="isBookmarked()"
|
|
18765
19141
|
[attr.tabindex]="tabIndex()"
|
|
18766
19142
|
(click)="bookmark($event)">
|
|
@@ -18770,7 +19146,7 @@ var BookmarkButtonComponent = class BookmarkButtonComponent {
|
|
|
18770
19146
|
@else {
|
|
18771
19147
|
<bookmark-icon />
|
|
18772
19148
|
}
|
|
18773
|
-
<span class="sr-only">{{
|
|
19149
|
+
<span class="sr-only">{{ 'bookmarks.bookmarkDocument' | transloco }}</span>
|
|
18774
19150
|
</button>
|
|
18775
19151
|
`,
|
|
18776
19152
|
isInline: true,
|
|
@@ -18818,6 +19194,11 @@ var BookmarkButtonComponent = class BookmarkButtonComponent {
|
|
|
18818
19194
|
"tooltip-offset"
|
|
18819
19195
|
],
|
|
18820
19196
|
exportAs: ["tooltip"]
|
|
19197
|
+
},
|
|
19198
|
+
{
|
|
19199
|
+
kind: "pipe",
|
|
19200
|
+
type: TranslocoPipe,
|
|
19201
|
+
name: "transloco"
|
|
18821
19202
|
}
|
|
18822
19203
|
]
|
|
18823
19204
|
});
|
|
@@ -18834,12 +19215,13 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
18834
19215
|
imports: [
|
|
18835
19216
|
BookmarkIcon,
|
|
18836
19217
|
ButtonComponent,
|
|
18837
|
-
TooltipDirective
|
|
19218
|
+
TooltipDirective,
|
|
19219
|
+
TranslocoPipe
|
|
18838
19220
|
],
|
|
18839
|
-
providers: [provideTranslocoScope("
|
|
19221
|
+
providers: [provideTranslocoScope("bookmarks")],
|
|
18840
19222
|
template: `
|
|
18841
19223
|
<button [variant]="variant()" [size]="size()" [iconOnly]="true"
|
|
18842
|
-
[tooltip]="
|
|
19224
|
+
[tooltip]="'bookmarks.bookmarkDocument' | transloco"
|
|
18843
19225
|
[attr.aria-pressed]="isBookmarked()"
|
|
18844
19226
|
[attr.tabindex]="tabIndex()"
|
|
18845
19227
|
(click)="bookmark($event)">
|
|
@@ -18849,7 +19231,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
18849
19231
|
@else {
|
|
18850
19232
|
<bookmark-icon />
|
|
18851
19233
|
}
|
|
18852
|
-
<span class="sr-only">{{
|
|
19234
|
+
<span class="sr-only">{{ 'bookmarks.bookmarkDocument' | transloco }}</span>
|
|
18853
19235
|
</button>
|
|
18854
19236
|
`,
|
|
18855
19237
|
host: {
|
|
@@ -19048,9 +19430,9 @@ var ListPanelComponent = class ListPanelComponent {
|
|
|
19048
19430
|
}
|
|
19049
19431
|
</ListItem>
|
|
19050
19432
|
} @empty {
|
|
19051
|
-
<
|
|
19433
|
+
<div class="py-(--spacing-xs) text-center text-(--font-neutral-base)">
|
|
19052
19434
|
<ng-content />
|
|
19053
|
-
</
|
|
19435
|
+
</div>
|
|
19054
19436
|
}
|
|
19055
19437
|
</List>
|
|
19056
19438
|
`,
|
|
@@ -19155,9 +19537,9 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
19155
19537
|
}
|
|
19156
19538
|
</ListItem>
|
|
19157
19539
|
} @empty {
|
|
19158
|
-
<
|
|
19540
|
+
<div class="py-(--spacing-xs) text-center text-(--font-neutral-base)">
|
|
19159
19541
|
<ng-content />
|
|
19160
|
-
</
|
|
19542
|
+
</div>
|
|
19161
19543
|
}
|
|
19162
19544
|
</List>
|
|
19163
19545
|
`,
|
|
@@ -19300,7 +19682,7 @@ var ListPanelFooterComponent = class ListPanelFooterComponent {
|
|
|
19300
19682
|
}
|
|
19301
19683
|
},
|
|
19302
19684
|
outputs: { loadMore: "loadMore" },
|
|
19303
|
-
host: { properties: { "class": "cn('flex flex-col px-2 mt-2 py-(--spacing-xs)', class())" } },
|
|
19685
|
+
host: { properties: { "class": "cn('flex flex-col gap-(--spacing-xs) px-2 mt-2 py-(--spacing-xs)', class())" } },
|
|
19304
19686
|
providers: [provideTranslocoScope("list-panel")],
|
|
19305
19687
|
ngImport: i0,
|
|
19306
19688
|
template: `
|
|
@@ -19452,7 +19834,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
19452
19834
|
</button>
|
|
19453
19835
|
</div>
|
|
19454
19836
|
`,
|
|
19455
|
-
host: { "[class]": "cn('flex flex-col px-2 mt-2 py-(--spacing-xs)', class())" }
|
|
19837
|
+
host: { "[class]": "cn('flex flex-col gap-(--spacing-xs) px-2 mt-2 py-(--spacing-xs)', class())" }
|
|
19456
19838
|
}]
|
|
19457
19839
|
}],
|
|
19458
19840
|
propDecorators: {
|
|
@@ -19531,8 +19913,8 @@ var ListPanelHeaderComponent = class ListPanelHeaderComponent {
|
|
|
19531
19913
|
template: `
|
|
19532
19914
|
@if (floating()) {
|
|
19533
19915
|
<div class="p-2">
|
|
19534
|
-
<
|
|
19535
|
-
<
|
|
19916
|
+
<!-- Not an <h2>: a page's prose styles put a rule and margins on every h2, even inside a popover. -->
|
|
19917
|
+
<span role="heading" aria-level="2" class="block text-xl leading-(--line-height-4) font-bold">{{ label() }}</span>
|
|
19536
19918
|
</div>
|
|
19537
19919
|
}
|
|
19538
19920
|
`,
|
|
@@ -19551,8 +19933,8 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
19551
19933
|
template: `
|
|
19552
19934
|
@if (floating()) {
|
|
19553
19935
|
<div class="p-2">
|
|
19554
|
-
<
|
|
19555
|
-
<
|
|
19936
|
+
<!-- Not an <h2>: a page's prose styles put a rule and margins on every h2, even inside a popover. -->
|
|
19937
|
+
<span role="heading" aria-level="2" class="block text-xl leading-(--line-height-4) font-bold">{{ label() }}</span>
|
|
19556
19938
|
</div>
|
|
19557
19939
|
}
|
|
19558
19940
|
`
|
|
@@ -19676,7 +20058,7 @@ var BookmarksComponent = class BookmarksComponent {
|
|
|
19676
20058
|
} },
|
|
19677
20059
|
providers: [provideTranslocoScope("bookmarks")],
|
|
19678
20060
|
ngImport: i0,
|
|
19679
|
-
template: "<ListPanelHeader\n [label]=\"'bookmarks.label' | transloco\"\n [floating]=\"!!floating\" />\n\n<ListPanel\n #panel\n [items]=\"paginatedBookmarks()\"\n [getValue]=\"idOf\"\n [getLabel]=\"labelOf\"\n deleteButtonTitle=\"bookmarks.removeBookmark\"\n [ariaLabel]=\"'bookmarks.label' | transloco\"\n (itemClick)=\"onClick($event)\"\n (delete)=\"onDelete($event)\">\n <ng-template listPanelItem let-bookmark>\n <ListItemLeft>\n <BookmarkIcon solid class=\"shrink-0\" />\n <ListItemText
|
|
20061
|
+
template: "<ListPanelHeader\n [label]=\"'bookmarks.label' | transloco\"\n [floating]=\"!!floating\" />\n\n<ListPanel\n #panel\n [items]=\"paginatedBookmarks()\"\n [getValue]=\"idOf\"\n [getLabel]=\"labelOf\"\n deleteButtonTitle=\"bookmarks.removeBookmark\"\n [ariaLabel]=\"'bookmarks.label' | transloco\"\n (itemClick)=\"onClick($event)\"\n (delete)=\"onDelete($event)\">\n <ng-template listPanelItem let-bookmark>\n <ListItemLeft>\n <BookmarkIcon solid class=\"shrink-0\" />\n <div class=\"flex min-w-0 flex-1 flex-col gap-(--spacing-3xs)\">\n <ListItemText\n truncated=\".list-item-text\"\n #labelCut=\"truncated\"\n [tooltip]=\"bookmark.label\"\n [tooltipDisabled]=\"!labelCut.isTruncated()\"\n tooltipPosition=\"top\"\n class=\"[&_.list-item-text]:line-clamp-2 in-focus-visible:[&_.list-item-text]:line-clamp-none\">\n {{ bookmark.label }}\n </ListItemText>\n\n @if (bookmark.author || bookmark.parentFolder) {\n <div class=\"flex min-w-0 flex-wrap gap-(--spacing-3xs)\">\n @if (bookmark.author) {\n <Tag\n variant=\"primary\"\n scheme=\"sage\"\n size=\"xs\"\n class=\"max-w-full gap-1\">\n <UserIcon class=\"size-3 shrink-0\" />\n <span\n truncated\n #authorCut=\"truncated\"\n [tooltip]=\"bookmark.author\"\n [tooltipDisabled]=\"!authorCut.isTruncated()\"\n tooltipPosition=\"top\"\n class=\"min-w-0 truncate in-focus-visible:break-words in-focus-visible:whitespace-normal\">\n {{ bookmark.author }}\n </span>\n </Tag>\n }\n @if (bookmark.parentFolder) {\n <Tag\n variant=\"primary\"\n scheme=\"sage\"\n size=\"xs\"\n class=\"max-w-full gap-1\">\n <FolderIcon class=\"size-3 shrink-0\" />\n <span\n truncated\n #folderCut=\"truncated\"\n [tooltip]=\"bookmark.parentFolder\"\n [tooltipDisabled]=\"!folderCut.isTruncated()\"\n tooltipPosition=\"top\"\n class=\"min-w-0 truncate in-focus-visible:break-words in-focus-visible:whitespace-normal\">\n {{ bookmark.parentFolder }}\n </span>\n </Tag>\n }\n </div>\n }\n </div>\n </ListItemLeft>\n </ng-template>\n\n <ListPanelEmpty color=\"pink\">\n <BookmarkIcon listPanelEmptyIcon solid />\n {{ \"bookmarks.noBookmarks\" | transloco }}\n <p listPanelEmptyHint>{{ \"bookmarks.noBookmarksHint\" | transloco }}</p>\n </ListPanelEmpty>\n</ListPanel>\n\n@if (paginatedBookmarks().length > 0) {\n <ListPanelFooter\n [navigationHint]=\"panel.keyboardFocused()\"\n [hasMore]=\"hasMore()\"\n [config]=\"config\"\n (loadMore)=\"loadMore()\" />\n}\n",
|
|
19680
20062
|
dependencies: [
|
|
19681
20063
|
{
|
|
19682
20064
|
kind: "component",
|
|
@@ -19699,6 +20081,12 @@ var BookmarksComponent = class BookmarksComponent {
|
|
|
19699
20081
|
],
|
|
19700
20082
|
outputs: ["itemClick", "delete"]
|
|
19701
20083
|
},
|
|
20084
|
+
{
|
|
20085
|
+
kind: "component",
|
|
20086
|
+
type: ListPanelEmptyComponent,
|
|
20087
|
+
selector: "list-panel-empty, ListPanelEmpty",
|
|
20088
|
+
inputs: ["color"]
|
|
20089
|
+
},
|
|
19702
20090
|
{
|
|
19703
20091
|
kind: "component",
|
|
19704
20092
|
type: ListPanelFooterComponent,
|
|
@@ -19758,6 +20146,39 @@ var BookmarksComponent = class BookmarksComponent {
|
|
|
19758
20146
|
"size"
|
|
19759
20147
|
]
|
|
19760
20148
|
},
|
|
20149
|
+
{
|
|
20150
|
+
kind: "directive",
|
|
20151
|
+
type: TooltipDirective,
|
|
20152
|
+
selector: "[tooltip]",
|
|
20153
|
+
inputs: [
|
|
20154
|
+
"tooltip",
|
|
20155
|
+
"tooltipPosition",
|
|
20156
|
+
"tooltipEvent",
|
|
20157
|
+
"showDelay",
|
|
20158
|
+
"hideDelay",
|
|
20159
|
+
"life",
|
|
20160
|
+
"tooltipOffset",
|
|
20161
|
+
"autoHide",
|
|
20162
|
+
"hideOnEscape",
|
|
20163
|
+
"escape",
|
|
20164
|
+
"tooltipDisabled",
|
|
20165
|
+
"tooltipArrow",
|
|
20166
|
+
"tooltipStyleClass",
|
|
20167
|
+
"tooltipOptions",
|
|
20168
|
+
"tooltip-position",
|
|
20169
|
+
"tooltip-delay",
|
|
20170
|
+
"tooltip-duration",
|
|
20171
|
+
"tooltip-offset"
|
|
20172
|
+
],
|
|
20173
|
+
exportAs: ["tooltip"]
|
|
20174
|
+
},
|
|
20175
|
+
{
|
|
20176
|
+
kind: "directive",
|
|
20177
|
+
type: TruncatedDirective,
|
|
20178
|
+
selector: "[truncated]",
|
|
20179
|
+
inputs: ["truncated"],
|
|
20180
|
+
exportAs: ["truncated"]
|
|
20181
|
+
},
|
|
19761
20182
|
{
|
|
19762
20183
|
kind: "pipe",
|
|
19763
20184
|
type: TranslocoPipe,
|
|
@@ -19779,6 +20200,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
19779
20200
|
TranslocoPipe,
|
|
19780
20201
|
ListPanelHeaderComponent,
|
|
19781
20202
|
ListPanelComponent,
|
|
20203
|
+
ListPanelEmptyComponent,
|
|
19782
20204
|
ListPanelFooterComponent,
|
|
19783
20205
|
ListPanelItemDirective,
|
|
19784
20206
|
BookmarkIcon,
|
|
@@ -19786,10 +20208,12 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
19786
20208
|
FolderIcon,
|
|
19787
20209
|
ListItemLeftDirective,
|
|
19788
20210
|
ListItemTextComponent,
|
|
19789
|
-
TagComponent
|
|
20211
|
+
TagComponent,
|
|
20212
|
+
TooltipDirective,
|
|
20213
|
+
TruncatedDirective
|
|
19790
20214
|
],
|
|
19791
20215
|
providers: [provideTranslocoScope("bookmarks")],
|
|
19792
|
-
template: "<ListPanelHeader\n [label]=\"'bookmarks.label' | transloco\"\n [floating]=\"!!floating\" />\n\n<ListPanel\n #panel\n [items]=\"paginatedBookmarks()\"\n [getValue]=\"idOf\"\n [getLabel]=\"labelOf\"\n deleteButtonTitle=\"bookmarks.removeBookmark\"\n [ariaLabel]=\"'bookmarks.label' | transloco\"\n (itemClick)=\"onClick($event)\"\n (delete)=\"onDelete($event)\">\n <ng-template listPanelItem let-bookmark>\n <ListItemLeft>\n <BookmarkIcon solid class=\"shrink-0\" />\n <ListItemText
|
|
20216
|
+
template: "<ListPanelHeader\n [label]=\"'bookmarks.label' | transloco\"\n [floating]=\"!!floating\" />\n\n<ListPanel\n #panel\n [items]=\"paginatedBookmarks()\"\n [getValue]=\"idOf\"\n [getLabel]=\"labelOf\"\n deleteButtonTitle=\"bookmarks.removeBookmark\"\n [ariaLabel]=\"'bookmarks.label' | transloco\"\n (itemClick)=\"onClick($event)\"\n (delete)=\"onDelete($event)\">\n <ng-template listPanelItem let-bookmark>\n <ListItemLeft>\n <BookmarkIcon solid class=\"shrink-0\" />\n <div class=\"flex min-w-0 flex-1 flex-col gap-(--spacing-3xs)\">\n <ListItemText\n truncated=\".list-item-text\"\n #labelCut=\"truncated\"\n [tooltip]=\"bookmark.label\"\n [tooltipDisabled]=\"!labelCut.isTruncated()\"\n tooltipPosition=\"top\"\n class=\"[&_.list-item-text]:line-clamp-2 in-focus-visible:[&_.list-item-text]:line-clamp-none\">\n {{ bookmark.label }}\n </ListItemText>\n\n @if (bookmark.author || bookmark.parentFolder) {\n <div class=\"flex min-w-0 flex-wrap gap-(--spacing-3xs)\">\n @if (bookmark.author) {\n <Tag\n variant=\"primary\"\n scheme=\"sage\"\n size=\"xs\"\n class=\"max-w-full gap-1\">\n <UserIcon class=\"size-3 shrink-0\" />\n <span\n truncated\n #authorCut=\"truncated\"\n [tooltip]=\"bookmark.author\"\n [tooltipDisabled]=\"!authorCut.isTruncated()\"\n tooltipPosition=\"top\"\n class=\"min-w-0 truncate in-focus-visible:break-words in-focus-visible:whitespace-normal\">\n {{ bookmark.author }}\n </span>\n </Tag>\n }\n @if (bookmark.parentFolder) {\n <Tag\n variant=\"primary\"\n scheme=\"sage\"\n size=\"xs\"\n class=\"max-w-full gap-1\">\n <FolderIcon class=\"size-3 shrink-0\" />\n <span\n truncated\n #folderCut=\"truncated\"\n [tooltip]=\"bookmark.parentFolder\"\n [tooltipDisabled]=\"!folderCut.isTruncated()\"\n tooltipPosition=\"top\"\n class=\"min-w-0 truncate in-focus-visible:break-words in-focus-visible:whitespace-normal\">\n {{ bookmark.parentFolder }}\n </span>\n </Tag>\n }\n </div>\n }\n </div>\n </ListItemLeft>\n </ng-template>\n\n <ListPanelEmpty color=\"pink\">\n <BookmarkIcon listPanelEmptyIcon solid />\n {{ \"bookmarks.noBookmarks\" | transloco }}\n <p listPanelEmptyHint>{{ \"bookmarks.noBookmarksHint\" | transloco }}</p>\n </ListPanelEmpty>\n</ListPanel>\n\n@if (paginatedBookmarks().length > 0) {\n <ListPanelFooter\n [navigationHint]=\"panel.keyboardFocused()\"\n [hasMore]=\"hasMore()\"\n [config]=\"config\"\n (loadMore)=\"loadMore()\" />\n}\n"
|
|
19793
20217
|
}]
|
|
19794
20218
|
}],
|
|
19795
20219
|
ctorParameters: () => [],
|
|
@@ -20045,7 +20469,7 @@ var CollectionsComponent = class CollectionsComponent {
|
|
|
20045
20469
|
} },
|
|
20046
20470
|
providers: [provideTranslocoScope("collections")],
|
|
20047
20471
|
ngImport: i0,
|
|
20048
|
-
template: "<ListPanelHeader\n [label]=\"'collections.label' | transloco\"\n [floating]=\"!!floating\" />\n\n<ListPanel\n #panel\n [items]=\"paginatedCollections()\"\n [getValue]=\"nameOf\"\n [getLabel]=\"nameOf\"\n deleteButtonTitle=\"collections.deleteCollection\"\n [ariaLabel]=\"'collections.label' | transloco\"\n (itemClick)=\"onClick($event)\"\n (delete)=\"onDelete($event)\">\n <ng-template listPanelItem let-collection>\n <ListItemLeft>\n <inbox-icon />\n <ListItemText>{{ collection.name }}</ListItemText>\n </ListItemLeft>\n </ng-template>\n\n {{ \"collections.noCollections\" | transloco }}\n</ListPanel>\n\n@if (paginatedCollections().length > 0) {\n <ListPanelFooter\n [navigationHint]=\"panel.keyboardFocused()\"\n [hasMore]=\"hasMore()\"\n [config]=\"config\"\n (loadMore)=\"loadMore()\" />\n}\n",
|
|
20472
|
+
template: "<ListPanelHeader\n [label]=\"'collections.label' | transloco\"\n [floating]=\"!!floating\" />\n\n<ListPanel\n #panel\n [items]=\"paginatedCollections()\"\n [getValue]=\"nameOf\"\n [getLabel]=\"nameOf\"\n deleteButtonTitle=\"collections.deleteCollection\"\n [ariaLabel]=\"'collections.label' | transloco\"\n (itemClick)=\"onClick($event)\"\n (delete)=\"onDelete($event)\">\n <ng-template listPanelItem let-collection>\n <ListItemLeft>\n <inbox-icon />\n <ListItemText>{{ collection.name }}</ListItemText>\n </ListItemLeft>\n </ng-template>\n\n <ListPanelEmpty color=\"indigo\">\n <inbox-icon listPanelEmptyIcon />\n {{ \"collections.noCollections\" | transloco }}\n <p listPanelEmptyHint>{{ \"collections.noCollectionsHint\" | transloco }}</p>\n </ListPanelEmpty>\n</ListPanel>\n\n@if (paginatedCollections().length > 0) {\n <ListPanelFooter\n [navigationHint]=\"panel.keyboardFocused()\"\n [hasMore]=\"hasMore()\"\n [config]=\"config\"\n (loadMore)=\"loadMore()\" />\n}\n",
|
|
20049
20473
|
dependencies: [
|
|
20050
20474
|
{
|
|
20051
20475
|
kind: "component",
|
|
@@ -20068,6 +20492,12 @@ var CollectionsComponent = class CollectionsComponent {
|
|
|
20068
20492
|
],
|
|
20069
20493
|
outputs: ["itemClick", "delete"]
|
|
20070
20494
|
},
|
|
20495
|
+
{
|
|
20496
|
+
kind: "component",
|
|
20497
|
+
type: ListPanelEmptyComponent,
|
|
20498
|
+
selector: "list-panel-empty, ListPanelEmpty",
|
|
20499
|
+
inputs: ["color"]
|
|
20500
|
+
},
|
|
20071
20501
|
{
|
|
20072
20502
|
kind: "component",
|
|
20073
20503
|
type: ListPanelFooterComponent,
|
|
@@ -20125,6 +20555,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
20125
20555
|
TranslocoPipe,
|
|
20126
20556
|
ListPanelHeaderComponent,
|
|
20127
20557
|
ListPanelComponent,
|
|
20558
|
+
ListPanelEmptyComponent,
|
|
20128
20559
|
ListPanelFooterComponent,
|
|
20129
20560
|
ListPanelItemDirective,
|
|
20130
20561
|
InboxIcon,
|
|
@@ -20132,7 +20563,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
20132
20563
|
ListItemTextComponent
|
|
20133
20564
|
],
|
|
20134
20565
|
providers: [provideTranslocoScope("collections")],
|
|
20135
|
-
template: "<ListPanelHeader\n [label]=\"'collections.label' | transloco\"\n [floating]=\"!!floating\" />\n\n<ListPanel\n #panel\n [items]=\"paginatedCollections()\"\n [getValue]=\"nameOf\"\n [getLabel]=\"nameOf\"\n deleteButtonTitle=\"collections.deleteCollection\"\n [ariaLabel]=\"'collections.label' | transloco\"\n (itemClick)=\"onClick($event)\"\n (delete)=\"onDelete($event)\">\n <ng-template listPanelItem let-collection>\n <ListItemLeft>\n <inbox-icon />\n <ListItemText>{{ collection.name }}</ListItemText>\n </ListItemLeft>\n </ng-template>\n\n {{ \"collections.noCollections\" | transloco }}\n</ListPanel>\n\n@if (paginatedCollections().length > 0) {\n <ListPanelFooter\n [navigationHint]=\"panel.keyboardFocused()\"\n [hasMore]=\"hasMore()\"\n [config]=\"config\"\n (loadMore)=\"loadMore()\" />\n}\n"
|
|
20566
|
+
template: "<ListPanelHeader\n [label]=\"'collections.label' | transloco\"\n [floating]=\"!!floating\" />\n\n<ListPanel\n #panel\n [items]=\"paginatedCollections()\"\n [getValue]=\"nameOf\"\n [getLabel]=\"nameOf\"\n deleteButtonTitle=\"collections.deleteCollection\"\n [ariaLabel]=\"'collections.label' | transloco\"\n (itemClick)=\"onClick($event)\"\n (delete)=\"onDelete($event)\">\n <ng-template listPanelItem let-collection>\n <ListItemLeft>\n <inbox-icon />\n <ListItemText>{{ collection.name }}</ListItemText>\n </ListItemLeft>\n </ng-template>\n\n <ListPanelEmpty color=\"indigo\">\n <inbox-icon listPanelEmptyIcon />\n {{ \"collections.noCollections\" | transloco }}\n <p listPanelEmptyHint>{{ \"collections.noCollectionsHint\" | transloco }}</p>\n </ListPanelEmpty>\n</ListPanel>\n\n@if (paginatedCollections().length > 0) {\n <ListPanelFooter\n [navigationHint]=\"panel.keyboardFocused()\"\n [hasMore]=\"hasMore()\"\n [config]=\"config\"\n (loadMore)=\"loadMore()\" />\n}\n"
|
|
20136
20567
|
}]
|
|
20137
20568
|
}],
|
|
20138
20569
|
ctorParameters: () => [],
|
|
@@ -21890,7 +22321,7 @@ var DidYouMeanComponent = class DidYouMeanComponent {
|
|
|
21890
22321
|
<p>
|
|
21891
22322
|
<span class="me-1">{{ 'didYouMean.didYouMean' | transloco }}</span>
|
|
21892
22323
|
<a class="text-(--font-primary-base) hover:underline cursor-pointer" role="button" (click)="selectCorrected()">{{ correction() }}</a>
|
|
21893
|
-
<span
|
|
22324
|
+
<span>{{ 'didYouMean.didYouMeanSuffix' | transloco }}</span>
|
|
21894
22325
|
</p>
|
|
21895
22326
|
}
|
|
21896
22327
|
@case ('correction') {
|
|
@@ -21940,7 +22371,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
21940
22371
|
<p>
|
|
21941
22372
|
<span class="me-1">{{ 'didYouMean.didYouMean' | transloco }}</span>
|
|
21942
22373
|
<a class="text-(--font-primary-base) hover:underline cursor-pointer" role="button" (click)="selectCorrected()">{{ correction() }}</a>
|
|
21943
|
-
<span
|
|
22374
|
+
<span>{{ 'didYouMean.didYouMeanSuffix' | transloco }}</span>
|
|
21944
22375
|
</p>
|
|
21945
22376
|
}
|
|
21946
22377
|
@case ('correction') {
|
|
@@ -30613,6 +31044,14 @@ function navigateToSearch(router, search) {
|
|
|
30613
31044
|
if (search.queryParams?.filters && search.queryParams?.filters?.length > 0) queryParams.f = JSON.stringify(search.queryParams?.filters);
|
|
30614
31045
|
router.navigate([search.queryParams?.path], { queryParams });
|
|
30615
31046
|
}
|
|
31047
|
+
function searchItemSubtext(transloco, search) {
|
|
31048
|
+
const parts = [];
|
|
31049
|
+
const filterCount = search.filterCount || 0;
|
|
31050
|
+
if (filterCount > 0) parts.push(transloco.translate("searches.filterCount", { count: filterCount }));
|
|
31051
|
+
if (search.date) parts.push(getRelativeDate(transloco.getActiveLang(), search.date));
|
|
31052
|
+
const text = parts.join(" · ");
|
|
31053
|
+
return text ? text.charAt(0).toUpperCase() + text.slice(1) : void 0;
|
|
31054
|
+
}
|
|
30616
31055
|
const RECENT_SEARCHES_OPTIONS = {
|
|
30617
31056
|
itemsPerPage: 10,
|
|
30618
31057
|
showLoadMore: true,
|
|
@@ -30629,7 +31068,7 @@ var RecentSearchesComponent = class RecentSearchesComponent {
|
|
|
30629
31068
|
userSettingsStore = inject(UserSettingsStore);
|
|
30630
31069
|
transloco = inject(TranslocoService);
|
|
30631
31070
|
router = inject(Router);
|
|
30632
|
-
|
|
31071
|
+
subtextOf = (search) => searchItemSubtext(this.transloco, search);
|
|
30633
31072
|
urlOf = (search) => search.url;
|
|
30634
31073
|
labelOf = (search) => search.display || search.label || search.url;
|
|
30635
31074
|
range = signal(10, ...ngDevMode ? [{ debugName: "range" }] : /* istanbul ignore next */ []);
|
|
@@ -30702,24 +31141,15 @@ var RecentSearchesComponent = class RecentSearchesComponent {
|
|
|
30702
31141
|
<ng-template listPanelItem let-search>
|
|
30703
31142
|
<ListItemLeft>
|
|
30704
31143
|
<history-icon />
|
|
30705
|
-
<ListItemText>{{ search.display || search.label }}</ListItemText>
|
|
31144
|
+
<ListItemText [subtext]="subtextOf(search)">{{ search.display || search.label }}</ListItemText>
|
|
30706
31145
|
</ListItemLeft>
|
|
30707
|
-
|
|
30708
|
-
@let filterCount = search.filterCount || 0;
|
|
30709
|
-
@if (filterCount > 0) {
|
|
30710
|
-
<Badge variant="number" fill="secondary" scheme="grey" size="xs" class="flex items-center gap-1" aria-hidden="true">
|
|
30711
|
-
<FilterIcon />
|
|
30712
|
-
{{ 'searches.filterCount' | transloco: { count: filterCount } }}
|
|
30713
|
-
</Badge>
|
|
30714
|
-
}
|
|
30715
|
-
|
|
30716
|
-
@let date = search.date;
|
|
30717
|
-
@if (date) {
|
|
30718
|
-
<Tag variant="primary" scheme="sage" size="xs" class="ms-auto shrink-0 first-letter:capitalize">{{ getRelativeDate(transloco.getActiveLang(), date) }}</Tag>
|
|
30719
|
-
}
|
|
30720
31146
|
</ng-template>
|
|
30721
31147
|
|
|
30722
|
-
|
|
31148
|
+
<ListPanelEmpty color="blue">
|
|
31149
|
+
<history-icon listPanelEmptyIcon />
|
|
31150
|
+
{{ "searches.recent.noRecentSearches" | transloco }}
|
|
31151
|
+
<p listPanelEmptyHint>{{ "searches.recent.noRecentSearchesHint" | transloco }}</p>
|
|
31152
|
+
</ListPanelEmpty>
|
|
30723
31153
|
</ListPanel>
|
|
30724
31154
|
|
|
30725
31155
|
@if (paginatedRecentSearches().length > 0) {
|
|
@@ -30737,36 +31167,6 @@ var RecentSearchesComponent = class RecentSearchesComponent {
|
|
|
30737
31167
|
selector: "history, history-icon, History, HistoryIcon",
|
|
30738
31168
|
inputs: ["class"]
|
|
30739
31169
|
},
|
|
30740
|
-
{
|
|
30741
|
-
kind: "component",
|
|
30742
|
-
type: FilterIcon,
|
|
30743
|
-
selector: "filter, filter-icon, Filter, FilterIcon",
|
|
30744
|
-
inputs: ["class"]
|
|
30745
|
-
},
|
|
30746
|
-
{
|
|
30747
|
-
kind: "directive",
|
|
30748
|
-
type: BadgeComponent,
|
|
30749
|
-
selector: "badge, Badge",
|
|
30750
|
-
inputs: [
|
|
30751
|
-
"class",
|
|
30752
|
-
"variant",
|
|
30753
|
-
"fill",
|
|
30754
|
-
"scheme",
|
|
30755
|
-
"size",
|
|
30756
|
-
"aria-label"
|
|
30757
|
-
]
|
|
30758
|
-
},
|
|
30759
|
-
{
|
|
30760
|
-
kind: "directive",
|
|
30761
|
-
type: TagComponent,
|
|
30762
|
-
selector: "tag, Tag",
|
|
30763
|
-
inputs: [
|
|
30764
|
-
"class",
|
|
30765
|
-
"variant",
|
|
30766
|
-
"scheme",
|
|
30767
|
-
"size"
|
|
30768
|
-
]
|
|
30769
|
-
},
|
|
30770
31170
|
{
|
|
30771
31171
|
kind: "directive",
|
|
30772
31172
|
type: ListItemLeftDirective,
|
|
@@ -30806,6 +31206,12 @@ var RecentSearchesComponent = class RecentSearchesComponent {
|
|
|
30806
31206
|
],
|
|
30807
31207
|
outputs: ["itemClick", "delete"]
|
|
30808
31208
|
},
|
|
31209
|
+
{
|
|
31210
|
+
kind: "component",
|
|
31211
|
+
type: ListPanelEmptyComponent,
|
|
31212
|
+
selector: "list-panel-empty, ListPanelEmpty",
|
|
31213
|
+
inputs: ["color"]
|
|
31214
|
+
},
|
|
30809
31215
|
{
|
|
30810
31216
|
kind: "component",
|
|
30811
31217
|
type: ListPanelFooterComponent,
|
|
@@ -30839,14 +31245,12 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
30839
31245
|
imports: [
|
|
30840
31246
|
TranslocoPipe,
|
|
30841
31247
|
HistoryIcon,
|
|
30842
|
-
FilterIcon,
|
|
30843
|
-
BadgeComponent,
|
|
30844
|
-
TagComponent,
|
|
30845
31248
|
ListItemLeftDirective,
|
|
30846
31249
|
ListItemTextComponent,
|
|
30847
31250
|
ListPanelItemDirective,
|
|
30848
31251
|
ListPanelHeaderComponent,
|
|
30849
31252
|
ListPanelComponent,
|
|
31253
|
+
ListPanelEmptyComponent,
|
|
30850
31254
|
ListPanelFooterComponent
|
|
30851
31255
|
],
|
|
30852
31256
|
template: `
|
|
@@ -30863,24 +31267,15 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
30863
31267
|
<ng-template listPanelItem let-search>
|
|
30864
31268
|
<ListItemLeft>
|
|
30865
31269
|
<history-icon />
|
|
30866
|
-
<ListItemText>{{ search.display || search.label }}</ListItemText>
|
|
31270
|
+
<ListItemText [subtext]="subtextOf(search)">{{ search.display || search.label }}</ListItemText>
|
|
30867
31271
|
</ListItemLeft>
|
|
30868
|
-
|
|
30869
|
-
@let filterCount = search.filterCount || 0;
|
|
30870
|
-
@if (filterCount > 0) {
|
|
30871
|
-
<Badge variant="number" fill="secondary" scheme="grey" size="xs" class="flex items-center gap-1" aria-hidden="true">
|
|
30872
|
-
<FilterIcon />
|
|
30873
|
-
{{ 'searches.filterCount' | transloco: { count: filterCount } }}
|
|
30874
|
-
</Badge>
|
|
30875
|
-
}
|
|
30876
|
-
|
|
30877
|
-
@let date = search.date;
|
|
30878
|
-
@if (date) {
|
|
30879
|
-
<Tag variant="primary" scheme="sage" size="xs" class="ms-auto shrink-0 first-letter:capitalize">{{ getRelativeDate(transloco.getActiveLang(), date) }}</Tag>
|
|
30880
|
-
}
|
|
30881
31272
|
</ng-template>
|
|
30882
31273
|
|
|
30883
|
-
|
|
31274
|
+
<ListPanelEmpty color="blue">
|
|
31275
|
+
<history-icon listPanelEmptyIcon />
|
|
31276
|
+
{{ "searches.recent.noRecentSearches" | transloco }}
|
|
31277
|
+
<p listPanelEmptyHint>{{ "searches.recent.noRecentSearchesHint" | transloco }}</p>
|
|
31278
|
+
</ListPanelEmpty>
|
|
30884
31279
|
</ListPanel>
|
|
30885
31280
|
|
|
30886
31281
|
@if (paginatedRecentSearches().length > 0) {
|
|
@@ -31352,7 +31747,7 @@ var SavedSearchesComponent = class SavedSearchesComponent {
|
|
|
31352
31747
|
userSettingsStore = inject(UserSettingsStore);
|
|
31353
31748
|
transloco = inject(TranslocoService);
|
|
31354
31749
|
router = inject(Router);
|
|
31355
|
-
|
|
31750
|
+
subtextOf = (search) => searchItemSubtext(this.transloco, search);
|
|
31356
31751
|
urlOf = (search) => search.url;
|
|
31357
31752
|
labelOf = (search) => search.display || search.label || search.url;
|
|
31358
31753
|
range = signal(10, ...ngDevMode ? [{ debugName: "range" }] : /* istanbul ignore next */ []);
|
|
@@ -31425,24 +31820,15 @@ var SavedSearchesComponent = class SavedSearchesComponent {
|
|
|
31425
31820
|
<ng-template listPanelItem let-search>
|
|
31426
31821
|
<ListItemLeft>
|
|
31427
31822
|
<star-icon [solid]="true" />
|
|
31428
|
-
<ListItemText>{{ search.display || search.label }}</ListItemText>
|
|
31823
|
+
<ListItemText [subtext]="subtextOf(search)">{{ search.display || search.label }}</ListItemText>
|
|
31429
31824
|
</ListItemLeft>
|
|
31430
|
-
|
|
31431
|
-
@let filterCount = search.filterCount || 0;
|
|
31432
|
-
@if (filterCount > 0) {
|
|
31433
|
-
<Badge variant="number" fill="secondary" scheme="grey" size="xs" class="flex items-center gap-1" aria-hidden="true">
|
|
31434
|
-
<FilterIcon />
|
|
31435
|
-
{{ 'searches.filterCount' | transloco: { count: filterCount } }}
|
|
31436
|
-
</Badge>
|
|
31437
|
-
}
|
|
31438
|
-
|
|
31439
|
-
@let date = search.date;
|
|
31440
|
-
@if (date) {
|
|
31441
|
-
<Tag variant="primary" scheme="sage" size="xs" class="ms-auto shrink-0 first-letter:capitalize">{{ getRelativeDate(transloco.getActiveLang(), date) }}</Tag>
|
|
31442
|
-
}
|
|
31443
31825
|
</ng-template>
|
|
31444
31826
|
|
|
31445
|
-
|
|
31827
|
+
<ListPanelEmpty color="yellow">
|
|
31828
|
+
<star-icon listPanelEmptyIcon [solid]="true" />
|
|
31829
|
+
{{ 'searches.saved.noSavedSearches' | transloco }}
|
|
31830
|
+
<p listPanelEmptyHint>{{ 'searches.saved.noSavedSearchesHint' | transloco }}</p>
|
|
31831
|
+
</ListPanelEmpty>
|
|
31446
31832
|
</ListPanel>
|
|
31447
31833
|
|
|
31448
31834
|
@if(paginatedSearches().length > 0) {
|
|
@@ -31457,36 +31843,6 @@ var SavedSearchesComponent = class SavedSearchesComponent {
|
|
|
31457
31843
|
selector: "star, star-icon, Star, StarIcon",
|
|
31458
31844
|
inputs: ["class", "solid"]
|
|
31459
31845
|
},
|
|
31460
|
-
{
|
|
31461
|
-
kind: "component",
|
|
31462
|
-
type: FilterIcon,
|
|
31463
|
-
selector: "filter, filter-icon, Filter, FilterIcon",
|
|
31464
|
-
inputs: ["class"]
|
|
31465
|
-
},
|
|
31466
|
-
{
|
|
31467
|
-
kind: "directive",
|
|
31468
|
-
type: BadgeComponent,
|
|
31469
|
-
selector: "badge, Badge",
|
|
31470
|
-
inputs: [
|
|
31471
|
-
"class",
|
|
31472
|
-
"variant",
|
|
31473
|
-
"fill",
|
|
31474
|
-
"scheme",
|
|
31475
|
-
"size",
|
|
31476
|
-
"aria-label"
|
|
31477
|
-
]
|
|
31478
|
-
},
|
|
31479
|
-
{
|
|
31480
|
-
kind: "directive",
|
|
31481
|
-
type: TagComponent,
|
|
31482
|
-
selector: "tag, Tag",
|
|
31483
|
-
inputs: [
|
|
31484
|
-
"class",
|
|
31485
|
-
"variant",
|
|
31486
|
-
"scheme",
|
|
31487
|
-
"size"
|
|
31488
|
-
]
|
|
31489
|
-
},
|
|
31490
31846
|
{
|
|
31491
31847
|
kind: "directive",
|
|
31492
31848
|
type: ListItemLeftDirective,
|
|
@@ -31526,6 +31882,12 @@ var SavedSearchesComponent = class SavedSearchesComponent {
|
|
|
31526
31882
|
],
|
|
31527
31883
|
outputs: ["itemClick", "delete"]
|
|
31528
31884
|
},
|
|
31885
|
+
{
|
|
31886
|
+
kind: "component",
|
|
31887
|
+
type: ListPanelEmptyComponent,
|
|
31888
|
+
selector: "list-panel-empty, ListPanelEmpty",
|
|
31889
|
+
inputs: ["color"]
|
|
31890
|
+
},
|
|
31529
31891
|
{
|
|
31530
31892
|
kind: "component",
|
|
31531
31893
|
type: ListPanelFooterComponent,
|
|
@@ -31559,14 +31921,12 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
31559
31921
|
imports: [
|
|
31560
31922
|
TranslocoPipe,
|
|
31561
31923
|
StarIcon,
|
|
31562
|
-
FilterIcon,
|
|
31563
|
-
BadgeComponent,
|
|
31564
|
-
TagComponent,
|
|
31565
31924
|
ListItemLeftDirective,
|
|
31566
31925
|
ListItemTextComponent,
|
|
31567
31926
|
ListPanelItemDirective,
|
|
31568
31927
|
ListPanelHeaderComponent,
|
|
31569
31928
|
ListPanelComponent,
|
|
31929
|
+
ListPanelEmptyComponent,
|
|
31570
31930
|
ListPanelFooterComponent
|
|
31571
31931
|
],
|
|
31572
31932
|
template: `
|
|
@@ -31583,24 +31943,15 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
31583
31943
|
<ng-template listPanelItem let-search>
|
|
31584
31944
|
<ListItemLeft>
|
|
31585
31945
|
<star-icon [solid]="true" />
|
|
31586
|
-
<ListItemText>{{ search.display || search.label }}</ListItemText>
|
|
31946
|
+
<ListItemText [subtext]="subtextOf(search)">{{ search.display || search.label }}</ListItemText>
|
|
31587
31947
|
</ListItemLeft>
|
|
31588
|
-
|
|
31589
|
-
@let filterCount = search.filterCount || 0;
|
|
31590
|
-
@if (filterCount > 0) {
|
|
31591
|
-
<Badge variant="number" fill="secondary" scheme="grey" size="xs" class="flex items-center gap-1" aria-hidden="true">
|
|
31592
|
-
<FilterIcon />
|
|
31593
|
-
{{ 'searches.filterCount' | transloco: { count: filterCount } }}
|
|
31594
|
-
</Badge>
|
|
31595
|
-
}
|
|
31596
|
-
|
|
31597
|
-
@let date = search.date;
|
|
31598
|
-
@if (date) {
|
|
31599
|
-
<Tag variant="primary" scheme="sage" size="xs" class="ms-auto shrink-0 first-letter:capitalize">{{ getRelativeDate(transloco.getActiveLang(), date) }}</Tag>
|
|
31600
|
-
}
|
|
31601
31948
|
</ng-template>
|
|
31602
31949
|
|
|
31603
|
-
|
|
31950
|
+
<ListPanelEmpty color="yellow">
|
|
31951
|
+
<star-icon listPanelEmptyIcon [solid]="true" />
|
|
31952
|
+
{{ 'searches.saved.noSavedSearches' | transloco }}
|
|
31953
|
+
<p listPanelEmptyHint>{{ 'searches.saved.noSavedSearchesHint' | transloco }}</p>
|
|
31954
|
+
</ListPanelEmpty>
|
|
31604
31955
|
</ListPanel>
|
|
31605
31956
|
|
|
31606
31957
|
@if(paginatedSearches().length > 0) {
|
|
@@ -31619,6 +31970,36 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
31619
31970
|
}]
|
|
31620
31971
|
}] }
|
|
31621
31972
|
});
|
|
31973
|
+
const isPlainObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
31974
|
+
const LEAF_SEPARATOR = " › ";
|
|
31975
|
+
function flattenLeaves(source, prefix = []) {
|
|
31976
|
+
return Object.entries(source).flatMap(([name, value]) => isPlainObject(value) ? flattenLeaves(value, [...prefix, name]) : [{
|
|
31977
|
+
path: [...prefix, name],
|
|
31978
|
+
value
|
|
31979
|
+
}]);
|
|
31980
|
+
}
|
|
31981
|
+
function valueAtPath(source, path) {
|
|
31982
|
+
return path.reduce((node, name) => isPlainObject(node) ? node[name] : void 0, source);
|
|
31983
|
+
}
|
|
31984
|
+
const toFieldText = (value) => {
|
|
31985
|
+
if (value === void 0 || value === null) return "";
|
|
31986
|
+
return Array.isArray(value) ? JSON.stringify(value) : String(value);
|
|
31987
|
+
};
|
|
31988
|
+
function normalizeCustomData(entries) {
|
|
31989
|
+
const properties = /* @__PURE__ */ new Map();
|
|
31990
|
+
const add = (key, value) => {
|
|
31991
|
+
if (!key || properties.has(key)) return;
|
|
31992
|
+
properties.set(key, value === void 0 || value === null ? { key } : {
|
|
31993
|
+
key,
|
|
31994
|
+
defaultValue: value
|
|
31995
|
+
});
|
|
31996
|
+
};
|
|
31997
|
+
for (const entry of Array.isArray(entries) ? entries : []) if (typeof entry === "string") add(entry);
|
|
31998
|
+
else if (entry && typeof entry === "object" && !Array.isArray(entry)) Object.entries(entry).forEach(([key, value]) => add(key, value));
|
|
31999
|
+
else console.warn("[user-profile] customData: entry ignored, expected a string or an object.", entry);
|
|
32000
|
+
return [...properties.values()];
|
|
32001
|
+
}
|
|
32002
|
+
const SAVED_FLASH_MS = 3e3;
|
|
31622
32003
|
const sameKeys = (a, b) => a.length === b.length && a.every((key, index) => key === b[index]);
|
|
31623
32004
|
const userProfileSchema = (editing) => schema((p) => {
|
|
31624
32005
|
applyEach(p.data, (field) => readonly(field, { when: ({ key }) => `data.${key()}` !== editing() }));
|
|
@@ -31632,9 +32013,13 @@ var UserProfileFormComponent = class UserProfileFormComponent {
|
|
|
31632
32013
|
transloco = inject(TranslocoService);
|
|
31633
32014
|
injector = inject(Injector);
|
|
31634
32015
|
document = inject(DOCUMENT);
|
|
32016
|
+
destroyRef = inject(DestroyRef);
|
|
31635
32017
|
labelClass = FORM_LABEL_CLASS;
|
|
31636
32018
|
errorClass = FORM_ERROR_CLASS;
|
|
32019
|
+
chipClass = "inline-flex items-center rounded-(--radius-rounded) bg-(--bg-accent-base-alt) px-(--spacing-2xs) text-(length:--size-sm) font-medium text-(--font-accent-base)";
|
|
31637
32020
|
statusMessage = signal("", ...ngDevMode ? [{ debugName: "statusMessage" }] : /* istanbul ignore next */ []);
|
|
32021
|
+
savedProperty = signal(void 0, ...ngDevMode ? [{ debugName: "savedProperty" }] : /* istanbul ignore next */ []);
|
|
32022
|
+
savedTimer;
|
|
31638
32023
|
dataKeys = [
|
|
31639
32024
|
"fullName",
|
|
31640
32025
|
"mail",
|
|
@@ -31657,10 +32042,29 @@ var UserProfileFormComponent = class UserProfileFormComponent {
|
|
|
31657
32042
|
equal: sameKeys
|
|
31658
32043
|
});
|
|
31659
32044
|
allowProfilePhoto = computed(() => this.keys().some((key) => key === "profilePhoto"), ...ngDevMode ? [{ debugName: "allowProfilePhoto" }] : /* istanbul ignore next */ []);
|
|
31660
|
-
|
|
32045
|
+
customDataConfig = computed(() => normalizeCustomData(this.appStore.general()?.features?.userProfile?.customData).filter(({ key }) => !this.keys().some((k) => k.toLowerCase() === key.toLowerCase())), ...ngDevMode ? [{ debugName: "customDataConfig" }] : /* istanbul ignore next */ []);
|
|
32046
|
+
customRows = computed(() => {
|
|
32047
|
+
const stored = this.user()?.customData;
|
|
32048
|
+
return this.customDataConfig().flatMap(({ key, defaultValue }) => {
|
|
32049
|
+
const shape = isPlainObject(defaultValue) ? defaultValue : isPlainObject(stored?.[key]) ? stored[key] : void 0;
|
|
32050
|
+
if (!shape) return [{
|
|
32051
|
+
key,
|
|
32052
|
+
root: key,
|
|
32053
|
+
defaultValue
|
|
32054
|
+
}];
|
|
32055
|
+
return flattenLeaves(shape).map(({ path, value }) => ({
|
|
32056
|
+
key: [key, ...path].join(LEAF_SEPARATOR),
|
|
32057
|
+
root: key,
|
|
32058
|
+
path: [key, ...path],
|
|
32059
|
+
defaultValue: isPlainObject(defaultValue) ? value : void 0
|
|
32060
|
+
}));
|
|
32061
|
+
});
|
|
32062
|
+
}, ...ngDevMode ? [{ debugName: "customRows" }] : /* istanbul ignore next */ []);
|
|
32063
|
+
customData = computed(() => this.customRows().map(({ key }) => key), {
|
|
31661
32064
|
...ngDevMode ? { debugName: "customData" } : /* istanbul ignore next */ {},
|
|
31662
32065
|
equal: sameKeys
|
|
31663
32066
|
});
|
|
32067
|
+
customRowIndex = computed(() => new Map(this.customRows().map((row) => [row.key, row])), ...ngDevMode ? [{ debugName: "customRowIndex" }] : /* istanbul ignore next */ []);
|
|
31664
32068
|
properties = computed(() => [...this.formKeys().map((key) => ({
|
|
31665
32069
|
category: "data",
|
|
31666
32070
|
key,
|
|
@@ -31670,6 +32074,13 @@ var UserProfileFormComponent = class UserProfileFormComponent {
|
|
|
31670
32074
|
key,
|
|
31671
32075
|
translated: false
|
|
31672
32076
|
}))], ...ngDevMode ? [{ debugName: "properties" }] : /* istanbul ignore next */ []);
|
|
32077
|
+
sections = computed(() => {
|
|
32078
|
+
const rows = this.properties();
|
|
32079
|
+
return ["data", "customData"].map((category) => ({
|
|
32080
|
+
id: category === "data" ? "information" : "preferences",
|
|
32081
|
+
rows: rows.filter((row) => row.category === category)
|
|
32082
|
+
})).filter((section) => section.rows.length > 0);
|
|
32083
|
+
}, ...ngDevMode ? [{ debugName: "sections" }] : /* istanbul ignore next */ []);
|
|
31673
32084
|
initials = computed(() => this.principalStore.initials(), ...ngDevMode ? [{ debugName: "initials" }] : /* istanbul ignore next */ []);
|
|
31674
32085
|
username = computed(() => this.principalStore.name() || this.principalStore.email() || null, ...ngDevMode ? [{ debugName: "username" }] : /* istanbul ignore next */ []);
|
|
31675
32086
|
propertyToEdit = signal(void 0, ...ngDevMode ? [{ debugName: "propertyToEdit" }] : /* istanbul ignore next */ []);
|
|
@@ -31695,7 +32106,7 @@ var UserProfileFormComponent = class UserProfileFormComponent {
|
|
|
31695
32106
|
const profile = this.user();
|
|
31696
32107
|
return {
|
|
31697
32108
|
data: toRecord(this.formKeys(), profile?.data),
|
|
31698
|
-
customData:
|
|
32109
|
+
customData: Object.fromEntries(this.customData().map((key) => [key, toFieldText(this.effectiveValue("customData", key))]))
|
|
31699
32110
|
};
|
|
31700
32111
|
}, ...ngDevMode ? [{ debugName: "profileModel" }] : /* istanbul ignore next */ []);
|
|
31701
32112
|
profileForm = form(this.profileModel, userProfileSchema(this.propertyToEdit), {
|
|
@@ -31706,6 +32117,7 @@ var UserProfileFormComponent = class UserProfileFormComponent {
|
|
|
31706
32117
|
effect(() => {
|
|
31707
32118
|
if (this.userProfileResource.error()?.status === 404) this.createUserProfile();
|
|
31708
32119
|
});
|
|
32120
|
+
this.destroyRef.onDestroy(() => clearTimeout(this.savedTimer));
|
|
31709
32121
|
}
|
|
31710
32122
|
fieldId(category, key) {
|
|
31711
32123
|
return `user-profile-${category}-${key.replace(/[^\w-]+/g, "-")}`;
|
|
@@ -31734,6 +32146,33 @@ var UserProfileFormComponent = class UserProfileFormComponent {
|
|
|
31734
32146
|
getDataValue(category, key) {
|
|
31735
32147
|
return this.user()?.[category]?.[key];
|
|
31736
32148
|
}
|
|
32149
|
+
defaultValue(category, key) {
|
|
32150
|
+
return category === "customData" ? this.customRowIndex().get(key)?.defaultValue : void 0;
|
|
32151
|
+
}
|
|
32152
|
+
isLeaf(category, key) {
|
|
32153
|
+
return category === "customData" && this.customRowIndex().get(key)?.path !== void 0;
|
|
32154
|
+
}
|
|
32155
|
+
storedValue(category, key) {
|
|
32156
|
+
const bucket = this.user()?.[category];
|
|
32157
|
+
const path = category === "customData" ? this.customRowIndex().get(key)?.path : void 0;
|
|
32158
|
+
return path ? valueAtPath(bucket, path) : bucket?.[key];
|
|
32159
|
+
}
|
|
32160
|
+
isBoolean(category, key) {
|
|
32161
|
+
return !this.isLeaf(category, key) && typeof this.effectiveValue(category, key) === "boolean";
|
|
32162
|
+
}
|
|
32163
|
+
booleanValue(category, key) {
|
|
32164
|
+
return this.effectiveValue(category, key) === true;
|
|
32165
|
+
}
|
|
32166
|
+
isReadOnlyValue(category, key) {
|
|
32167
|
+
return Array.isArray(this.effectiveValue(category, key));
|
|
32168
|
+
}
|
|
32169
|
+
arrayTags(category, key) {
|
|
32170
|
+
const value = this.effectiveValue(category, key);
|
|
32171
|
+
return [...new Set((Array.isArray(value) ? value : []).map((item) => typeof item === "string" ? item : JSON.stringify(item)))];
|
|
32172
|
+
}
|
|
32173
|
+
effectiveValue(category, key) {
|
|
32174
|
+
return this.storedValue(category, key) ?? this.defaultValue(category, key);
|
|
32175
|
+
}
|
|
31737
32176
|
onEdit(category, key) {
|
|
31738
32177
|
this.propertyToEdit.set(`${category}.${key}`);
|
|
31739
32178
|
this.fieldFor(category, key)?.().focusBoundControl();
|
|
@@ -31743,19 +32182,43 @@ var UserProfileFormComponent = class UserProfileFormComponent {
|
|
|
31743
32182
|
const field = event.target;
|
|
31744
32183
|
field.setSelectionRange(field.value.length, field.value.length);
|
|
31745
32184
|
}
|
|
32185
|
+
onEscape(event, category, key) {
|
|
32186
|
+
if (!this.isEditing(category, key)) return;
|
|
32187
|
+
event.preventDefault();
|
|
32188
|
+
event.stopPropagation();
|
|
32189
|
+
this.onCancelEdit(category, key);
|
|
32190
|
+
}
|
|
31746
32191
|
onCancelEdit(category, key) {
|
|
31747
32192
|
if (!this.isEditing(category, key)) return;
|
|
31748
32193
|
const state = this.fieldFor(category, key)?.();
|
|
31749
|
-
state?.value.set(this.
|
|
32194
|
+
state?.value.set(toFieldText(this.effectiveValue(category, key)));
|
|
31750
32195
|
state?.reset();
|
|
31751
32196
|
this.leaveEditMode(category, key);
|
|
31752
32197
|
this.announce("editCancelled", category, key);
|
|
31753
32198
|
}
|
|
31754
|
-
|
|
31755
|
-
|
|
32199
|
+
labelFor(category, key) {
|
|
32200
|
+
if (category === "data") return this.transloco.translate(`userProfile.data.${key}`);
|
|
32201
|
+
const leafPath = this.customRowIndex().get(key)?.path;
|
|
32202
|
+
if (leafPath) return [this.labelFor(category, leafPath[0]), ...leafPath.slice(1)].join(LEAF_SEPARATOR);
|
|
32203
|
+
const path = `userProfile.customData.${key}`;
|
|
32204
|
+
const translated = this.transloco.translate(path);
|
|
32205
|
+
return translated && translated !== path ? translated : key;
|
|
32206
|
+
}
|
|
32207
|
+
isDefault(category, key) {
|
|
32208
|
+
const stored = this.storedValue(category, key);
|
|
32209
|
+
return (stored === void 0 || stored === null) && this.defaultValue(category, key) !== void 0;
|
|
32210
|
+
}
|
|
32211
|
+
isSaved(category, key) {
|
|
32212
|
+
return this.savedProperty() === `${category}.${key}`;
|
|
32213
|
+
}
|
|
32214
|
+
flashSaved(category, key) {
|
|
32215
|
+
this.savedProperty.set(`${category}.${key}`);
|
|
32216
|
+
clearTimeout(this.savedTimer);
|
|
32217
|
+
this.savedTimer = setTimeout(() => this.savedProperty.set(void 0), SAVED_FLASH_MS);
|
|
31756
32218
|
}
|
|
31757
32219
|
announce(kind, category, key) {
|
|
31758
|
-
this.statusMessage.set(this.transloco.translate(`userProfile.status.${kind}`, { property: this.
|
|
32220
|
+
this.statusMessage.set(this.transloco.translate(`userProfile.status.${kind}`, { property: this.labelFor(category, key) }));
|
|
32221
|
+
if (kind === "saved") this.flashSaved(category, key);
|
|
31759
32222
|
}
|
|
31760
32223
|
leaveEditMode(category, key) {
|
|
31761
32224
|
this.propertyToEdit.set(void 0);
|
|
@@ -31789,6 +32252,7 @@ var UserProfileFormComponent = class UserProfileFormComponent {
|
|
|
31789
32252
|
}
|
|
31790
32253
|
}
|
|
31791
32254
|
async onDeleteData(category, key) {
|
|
32255
|
+
if (!this.hasStoredValue(category, key)) return;
|
|
31792
32256
|
try {
|
|
31793
32257
|
const response = await this.client.api.userProfile.deleteProperty(this.user(), category, key);
|
|
31794
32258
|
if (!response) {
|
|
@@ -31796,14 +32260,46 @@ var UserProfileFormComponent = class UserProfileFormComponent {
|
|
|
31796
32260
|
return;
|
|
31797
32261
|
}
|
|
31798
32262
|
info("Deleted user profile property", response);
|
|
31799
|
-
|
|
31800
|
-
userProfile[category][key] = "";
|
|
31801
|
-
this.userProfileResource.set(userProfile);
|
|
31802
|
-
this.announce("deleted", category, key);
|
|
32263
|
+
this.forgetProperty(category, key);
|
|
31803
32264
|
} catch (err) {
|
|
32265
|
+
if (err?.status === 404) {
|
|
32266
|
+
info("User profile property already absent", {
|
|
32267
|
+
category,
|
|
32268
|
+
key
|
|
32269
|
+
});
|
|
32270
|
+
this.forgetProperty(category, key);
|
|
32271
|
+
return;
|
|
32272
|
+
}
|
|
31804
32273
|
notify.error(err.message);
|
|
31805
32274
|
}
|
|
31806
32275
|
}
|
|
32276
|
+
hasStoredValue(category, key) {
|
|
32277
|
+
const stored = this.storedValue(category, key);
|
|
32278
|
+
return stored !== void 0 && stored !== null;
|
|
32279
|
+
}
|
|
32280
|
+
forgetProperty(category, key) {
|
|
32281
|
+
const userProfile = JSON.parse(JSON.stringify(this.user()));
|
|
32282
|
+
if (this.defaultValue(category, key) === void 0) userProfile[category][key] = "";
|
|
32283
|
+
else delete userProfile[category][key];
|
|
32284
|
+
this.userProfileResource.set(userProfile);
|
|
32285
|
+
this.announce("deleted", category, key);
|
|
32286
|
+
}
|
|
32287
|
+
async onToggleBoolean(category, key, checked, toggle) {
|
|
32288
|
+
const previous = this.user();
|
|
32289
|
+
if (!previous || this.booleanValue(category, key) === checked) return;
|
|
32290
|
+
this.userProfileResource.set({
|
|
32291
|
+
...previous,
|
|
32292
|
+
[category]: {
|
|
32293
|
+
...previous[category],
|
|
32294
|
+
[key]: checked
|
|
32295
|
+
}
|
|
32296
|
+
});
|
|
32297
|
+
if (await this.patchProperty(category, key, checked)) this.announce("saved", category, key);
|
|
32298
|
+
else {
|
|
32299
|
+
this.userProfileResource.set(previous);
|
|
32300
|
+
toggle.checked.set(!checked);
|
|
32301
|
+
}
|
|
32302
|
+
}
|
|
31807
32303
|
async patchProperty(category, key, value) {
|
|
31808
32304
|
const userProfile = JSON.parse(JSON.stringify(this.user()));
|
|
31809
32305
|
userProfile[category] = {
|
|
@@ -31857,7 +32353,7 @@ var UserProfileFormComponent = class UserProfileFormComponent {
|
|
|
31857
32353
|
host: { classAttribute: "flex flex-col h-full overflow-hidden" },
|
|
31858
32354
|
providers: [provideTranslocoScope("user-profile", "login")],
|
|
31859
32355
|
ngImport: i0,
|
|
31860
|
-
template: "@if (userProfileResource.hasValue()) {\n @let profile = user();\n <div class=\"flex min-h-0 flex-1 flex-col gap-2 p-4\">\n <div class=\"mb-4 flex\">\n @if (allowProfilePhoto()) {\n @let profilePhoto = profile?.data?.profilePhoto;\n <!-- AVATAR — a real <button>: the avatar opens the file picker, so it has to be reachable and\n activatable with the keyboard, which a click handler on a <div> never is. -->\n <button\n type=\"button\"\n variant=\"none\"\n class=\"cursor-pointer rounded-full\"\n [title]=\"'userProfile.changePhoto' | transloco\"\n [attr.aria-label]=\"'userProfile.changePhoto' | transloco\"\n (click)=\"avatarInput.click()\">\n <Avatar fill=\"secondary\" class=\"size-14\">\n @if (profilePhoto) {\n <!-- alt=\"\": the button already carries the accessible name, the image would repeat it. -->\n <AvatarImage [src]=\"profilePhoto\" width=\"44\" height=\"44\" alt=\"\" />\n }\n <AvatarFallback class=\"size-full text-lg\">\n @if (initials()) {\n <span>{{ initials() }}</span>\n } @else {\n <UserIcon class=\"size-7\" />\n }\n </AvatarFallback>\n </Avatar>\n </button>\n @if (profilePhoto) {\n <button\n type=\"button\"\n variant=\"tertiary\"\n [iconOnly]=\"true\"\n class=\"text-destructive cursor-pointer\"\n [title]=\"'userProfile.deleteProperty' | transloco\"\n [attr.aria-label]=\"\n 'userProfile.deletePropertyNamed'\n | transloco\n : { property: 'userProfile.data.profilePhoto' | transloco }\n \"\n (click)=\"onDeleteData('data', 'profilePhoto')\">\n <TrashIcon />\n </button>\n }\n }\n <div class=\"grow\"></div>\n <div class=\"flex flex-col gap-4\">\n <!-- CHANGE PASSWORD -->\n @if (allowChangePassword()) {\n <button\n type=\"button\"\n [disabled]=\"changingPassword()\"\n (click)=\"changingPassword.set(true)\">\n {{ \"login.changePassword\" | transloco }}\n </button>\n }\n </div>\n </div>\n @if (changingPassword()) {\n <!-- ChangePassword renders its own <form [formRoot]>: it stays in this branch, mutually\n exclusive with the profile form below, so the two never nest. -->\n <ChangePassword\n [username]=\"username()\"\n [redirectAfterSuccess]=\"false\"\n [redirectAfterCancel]=\"false\"\n (cancel)=\"changingPassword.set(false)\"\n (success)=\"changingPassword.set(false)\" />\n } @else {\n <!-- p: the focus outline of the fields is drawn *outside* their box, and this is the\n scrolling ancestor — without a gutter it clips the ring on every edge it touches\n (left for all of them, bottom for the last one, top for the first). -->\n <div class=\"min-h-0 flex-1 overflow-auto p-(--spacing-3xs)\">\n <!--\n [formRoot] pose novalidate, intercepte submit et appelle preventDefault() avant de déclencher\n submit() sur le field tree. Un seul bouton type=\"submit\" est rendu à la fois — celui de la\n ligne en cours d'édition — et l'action de soumission persiste cette seule propriété.\n -->\n <!-- aria-label: a <form> with no accessible name is not exposed as a form landmark, so the\n fields could not be reached as a whole. aria-busy holds the announcement back while a\n property is being persisted. -->\n <form\n [formRoot]=\"profileForm\"\n [attr.aria-label]=\"'userProfile.profileFields' | transloco\"\n [attr.aria-busy]=\"profileForm().submitting() ? 'true' : null\">\n @for (\n property of properties();\n track property.category + \".\" + property.key\n ) {\n @let category = property.category;\n @let key = property.key;\n @let field = profileForm[category][key];\n @let label =\n property.translated\n ? (\"userProfile.data.\" + key | transloco)\n : key;\n @let editing = isEditing(category, key);\n @let invalid = showError(field);\n <!-- role=\"group\" named by the label: without it the four action buttons of a row are\n announced out of context, as a flat run of \"Edit …\" / \"Delete …\" with nothing tying\n them to the property they belong to. -->\n <div\n role=\"group\"\n [attr.aria-labelledby]=\"labelId(category, key)\"\n class=\"flex flex-col gap-(--spacing-3xs)\">\n <label\n [id]=\"labelId(category, key)\"\n [for]=\"fieldId(category, key)\"\n [class]=\"labelClass\"\n >{{ label }}</label\n >\n <div class=\"group flex flex-row\">\n <div class=\"min-w-0 grow\">\n <textarea-group\n resize=\"auto\"\n [maxRows]=\"3\"\n [scheme]=\"invalid ? 'error' : 'default'\">\n <!-- No [attr.readonly] here: [formField] writes the readonly attribute itself from\n the field state, which the schema's readonly() rule drives. The native\n readonly attribute is what exposes aria-readonly — do not duplicate it. -->\n <textarea\n textarea-control\n rows=\"1\"\n [id]=\"fieldId(category, key)\"\n [formField]=\"field\"\n [attr.placeholder]=\"'userProfile.notDefined' | transloco\"\n [attr.aria-invalid]=\"invalid ? 'true' : null\"\n [attr.aria-describedby]=\"\n invalid ? errorId(category, key) : null\n \"\n [attr.aria-keyshortcuts]=\"editing ? 'Escape' : null\"\n (focus)=\"onFieldFocus($event)\"\n (keydown.escape)=\"onCancelEdit(category, key)\"></textarea>\n </textarea-group>\n </div>\n <!-- group-focus-within, and not only group-hover: `invisible` takes the buttons out of\n the tab order, so without it editing or deleting a property is unreachable with a\n keyboard. Focusing the field of the row reveals its actions. -->\n @if (editing) {\n <button\n type=\"submit\"\n variant=\"tertiary\"\n [iconOnly]=\"true\"\n class=\"visible mx-2 cursor-pointer sm:invisible sm:group-focus-within:visible sm:group-hover:visible\"\n [disabled]=\"profileForm().submitting()\"\n [title]=\"'userProfile.saveProperty' | transloco\"\n [attr.aria-label]=\"\n 'userProfile.savePropertyNamed'\n | transloco: { property: label }\n \">\n <save-icon />\n </button>\n <button\n type=\"button\"\n variant=\"tertiary\"\n [iconOnly]=\"true\"\n class=\"visible cursor-pointer sm:invisible sm:group-focus-within:visible sm:group-hover:visible\"\n [title]=\"'userProfile.cancelEdit' | transloco\"\n [attr.aria-label]=\"\n 'userProfile.cancelEditNamed'\n | transloco: { property: label }\n \"\n (click)=\"onCancelEdit(category, key)\">\n <UndoIcon />\n </button>\n } @else {\n <button\n type=\"button\"\n variant=\"tertiary\"\n [iconOnly]=\"true\"\n [id]=\"editButtonId(category, key)\"\n class=\"visible mx-2 cursor-pointer sm:invisible sm:group-focus-within:visible sm:group-hover:visible\"\n [title]=\"'userProfile.editProperty' | transloco\"\n [attr.aria-label]=\"\n 'userProfile.editPropertyNamed'\n | transloco: { property: label }\n \"\n (click)=\"onEdit(category, key)\">\n <EditIcon />\n </button>\n <button\n type=\"button\"\n variant=\"tertiary\"\n [iconOnly]=\"true\"\n class=\"text-destructive visible cursor-pointer sm:invisible sm:group-focus-within:visible sm:group-hover:visible\"\n [title]=\"'userProfile.deleteProperty' | transloco\"\n [attr.aria-label]=\"\n 'userProfile.deletePropertyNamed'\n | transloco: { property: label }\n \"\n (click)=\"onDeleteData(category, key)\">\n <TrashIcon />\n </button>\n }\n </div>\n @if (invalid) {\n <p\n [id]=\"errorId(category, key)\"\n role=\"alert\"\n [class]=\"errorClass\">\n {{ errorKey(field) | transloco }}\n </p>\n }\n </div>\n }\n </form>\n <!-- Always in the DOM, and only its text changes: a live region inserted at the same time as\n its content is not reliably announced. `sr-only` — the transitions it describes are\n already visible on screen. -->\n <p class=\"sr-only\" role=\"status\" aria-live=\"polite\">\n {{ statusMessage() }}\n </p>\n </div>\n }\n </div>\n} @else if (userProfileResource.isLoading()) {\n <p role=\"status\">{{ \"userProfile.loading\" | transloco }}</p>\n} @else if (userProfileResource.error()) {\n <p role=\"alert\">{{ \"userProfile.loadError\" | transloco }}</p>\n}\n\n<!-- AVATAR UPLOAD — `hidden` keeps it out of both the tab order and the accessibility tree, which is\n intended: it is opened by the avatar button above. It also sits outside the <form>. -->\n<input\n #avatarInput\n class=\"hidden\"\n type=\"file\"\n accept=\"image/*\"\n (change)=\"uploadAvatar($event)\" />\n",
|
|
32356
|
+
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",
|
|
31861
32357
|
dependencies: [
|
|
31862
32358
|
{
|
|
31863
32359
|
kind: "directive",
|
|
@@ -31950,24 +32446,75 @@ var UserProfileFormComponent = class UserProfileFormComponent {
|
|
|
31950
32446
|
"size"
|
|
31951
32447
|
]
|
|
31952
32448
|
},
|
|
32449
|
+
{
|
|
32450
|
+
kind: "component",
|
|
32451
|
+
type: CircleCheckIcon,
|
|
32452
|
+
selector: "circle-check, circle-check-icon, CircleCheck, CircleCheckIcon",
|
|
32453
|
+
inputs: ["class"]
|
|
32454
|
+
},
|
|
32455
|
+
{
|
|
32456
|
+
kind: "component",
|
|
32457
|
+
type: LockIcon,
|
|
32458
|
+
selector: "lock, lock-icon, Lock, LockIcon",
|
|
32459
|
+
inputs: ["class"]
|
|
32460
|
+
},
|
|
32461
|
+
{
|
|
32462
|
+
kind: "component",
|
|
32463
|
+
type: SwitchComponent,
|
|
32464
|
+
selector: "switch, Switch",
|
|
32465
|
+
inputs: [
|
|
32466
|
+
"checked",
|
|
32467
|
+
"disabled",
|
|
32468
|
+
"size",
|
|
32469
|
+
"class",
|
|
32470
|
+
"aria-label",
|
|
32471
|
+
"aria-labelledby",
|
|
32472
|
+
"presentational",
|
|
32473
|
+
"compact",
|
|
32474
|
+
"tabIndex"
|
|
32475
|
+
],
|
|
32476
|
+
outputs: ["checkedChange"]
|
|
32477
|
+
},
|
|
32478
|
+
{
|
|
32479
|
+
kind: "component",
|
|
32480
|
+
type: TagInputComponent,
|
|
32481
|
+
selector: "TagInput, tag-input, taginput",
|
|
32482
|
+
inputs: [
|
|
32483
|
+
"value",
|
|
32484
|
+
"disabled",
|
|
32485
|
+
"readonly",
|
|
32486
|
+
"placeholder",
|
|
32487
|
+
"separator",
|
|
32488
|
+
"addOnBlur",
|
|
32489
|
+
"size",
|
|
32490
|
+
"scheme",
|
|
32491
|
+
"class",
|
|
32492
|
+
"aria-label",
|
|
32493
|
+
"addLabel",
|
|
32494
|
+
"removeLabel",
|
|
32495
|
+
"addedLabel",
|
|
32496
|
+
"removedLabel"
|
|
32497
|
+
],
|
|
32498
|
+
outputs: ["valueChange", "touch"]
|
|
32499
|
+
},
|
|
31953
32500
|
{
|
|
31954
32501
|
kind: "directive",
|
|
31955
|
-
type:
|
|
31956
|
-
selector: "
|
|
32502
|
+
type: InputComponent,
|
|
32503
|
+
selector: "input-group, inputgroup, InputGroup",
|
|
31957
32504
|
inputs: [
|
|
31958
32505
|
"class",
|
|
31959
32506
|
"size",
|
|
31960
32507
|
"scheme",
|
|
31961
|
-
"resize",
|
|
31962
32508
|
"filled",
|
|
31963
|
-
"
|
|
31964
|
-
"
|
|
32509
|
+
"numbers",
|
|
32510
|
+
"autoHeight",
|
|
32511
|
+
"textSize"
|
|
31965
32512
|
]
|
|
31966
32513
|
},
|
|
31967
32514
|
{
|
|
31968
32515
|
kind: "directive",
|
|
31969
|
-
type:
|
|
31970
|
-
selector: "
|
|
32516
|
+
type: InputControlDirective,
|
|
32517
|
+
selector: "input[input-control], input[inputControl]",
|
|
31971
32518
|
inputs: ["class"]
|
|
31972
32519
|
},
|
|
31973
32520
|
{
|
|
@@ -32019,18 +32566,22 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
32019
32566
|
UserIcon,
|
|
32020
32567
|
AvatarImageComponent,
|
|
32021
32568
|
ButtonComponent,
|
|
32022
|
-
|
|
32023
|
-
|
|
32569
|
+
CircleCheckIcon,
|
|
32570
|
+
LockIcon,
|
|
32571
|
+
SwitchComponent,
|
|
32572
|
+
TagInputComponent,
|
|
32573
|
+
InputComponent,
|
|
32574
|
+
InputControlDirective,
|
|
32024
32575
|
ChangePasswordComponent
|
|
32025
32576
|
],
|
|
32026
32577
|
providers: [provideTranslocoScope("user-profile", "login")],
|
|
32027
|
-
template: "@if (userProfileResource.hasValue()) {\n @let profile = user();\n <div class=\"flex min-h-0 flex-1 flex-col gap-2 p-4\">\n <div class=\"mb-4 flex\">\n @if (allowProfilePhoto()) {\n @let profilePhoto = profile?.data?.profilePhoto;\n <!-- AVATAR — a real <button>: the avatar opens the file picker, so it has to be reachable and\n activatable with the keyboard, which a click handler on a <div> never is. -->\n <button\n type=\"button\"\n variant=\"none\"\n class=\"cursor-pointer rounded-full\"\n [title]=\"'userProfile.changePhoto' | transloco\"\n [attr.aria-label]=\"'userProfile.changePhoto' | transloco\"\n (click)=\"avatarInput.click()\">\n <Avatar fill=\"secondary\" class=\"size-14\">\n @if (profilePhoto) {\n <!-- alt=\"\": the button already carries the accessible name, the image would repeat it. -->\n <AvatarImage [src]=\"profilePhoto\" width=\"44\" height=\"44\" alt=\"\" />\n }\n <AvatarFallback class=\"size-full text-lg\">\n @if (initials()) {\n <span>{{ initials() }}</span>\n } @else {\n <UserIcon class=\"size-7\" />\n }\n </AvatarFallback>\n </Avatar>\n </button>\n @if (profilePhoto) {\n <button\n type=\"button\"\n variant=\"tertiary\"\n [iconOnly]=\"true\"\n class=\"text-destructive cursor-pointer\"\n [title]=\"'userProfile.deleteProperty' | transloco\"\n [attr.aria-label]=\"\n 'userProfile.deletePropertyNamed'\n | transloco\n : { property: 'userProfile.data.profilePhoto' | transloco }\n \"\n (click)=\"onDeleteData('data', 'profilePhoto')\">\n <TrashIcon />\n </button>\n }\n }\n <div class=\"grow\"></div>\n <div class=\"flex flex-col gap-4\">\n <!-- CHANGE PASSWORD -->\n @if (allowChangePassword()) {\n <button\n type=\"button\"\n [disabled]=\"changingPassword()\"\n (click)=\"changingPassword.set(true)\">\n {{ \"login.changePassword\" | transloco }}\n </button>\n }\n </div>\n </div>\n @if (changingPassword()) {\n <!-- ChangePassword renders its own <form [formRoot]>: it stays in this branch, mutually\n exclusive with the profile form below, so the two never nest. -->\n <ChangePassword\n [username]=\"username()\"\n [redirectAfterSuccess]=\"false\"\n [redirectAfterCancel]=\"false\"\n (cancel)=\"changingPassword.set(false)\"\n (success)=\"changingPassword.set(false)\" />\n } @else {\n <!-- p: the focus outline of the fields is drawn *outside* their box, and this is the\n scrolling ancestor — without a gutter it clips the ring on every edge it touches\n (left for all of them, bottom for the last one, top for the first). -->\n <div class=\"min-h-0 flex-1 overflow-auto p-(--spacing-3xs)\">\n <!--\n [formRoot] pose novalidate, intercepte submit et appelle preventDefault() avant de déclencher\n submit() sur le field tree. Un seul bouton type=\"submit\" est rendu à la fois — celui de la\n ligne en cours d'édition — et l'action de soumission persiste cette seule propriété.\n -->\n <!-- aria-label: a <form> with no accessible name is not exposed as a form landmark, so the\n fields could not be reached as a whole. aria-busy holds the announcement back while a\n property is being persisted. -->\n <form\n [formRoot]=\"profileForm\"\n [attr.aria-label]=\"'userProfile.profileFields' | transloco\"\n [attr.aria-busy]=\"profileForm().submitting() ? 'true' : null\">\n @for (\n property of properties();\n track property.category + \".\" + property.key\n ) {\n @let category = property.category;\n @let key = property.key;\n @let field = profileForm[category][key];\n @let label =\n property.translated\n ? (\"userProfile.data.\" + key | transloco)\n : key;\n @let editing = isEditing(category, key);\n @let invalid = showError(field);\n <!-- role=\"group\" named by the label: without it the four action buttons of a row are\n announced out of context, as a flat run of \"Edit …\" / \"Delete …\" with nothing tying\n them to the property they belong to. -->\n <div\n role=\"group\"\n [attr.aria-labelledby]=\"labelId(category, key)\"\n class=\"flex flex-col gap-(--spacing-3xs)\">\n <label\n [id]=\"labelId(category, key)\"\n [for]=\"fieldId(category, key)\"\n [class]=\"labelClass\"\n >{{ label }}</label\n >\n <div class=\"group flex flex-row\">\n <div class=\"min-w-0 grow\">\n <textarea-group\n resize=\"auto\"\n [maxRows]=\"3\"\n [scheme]=\"invalid ? 'error' : 'default'\">\n <!-- No [attr.readonly] here: [formField] writes the readonly attribute itself from\n the field state, which the schema's readonly() rule drives. The native\n readonly attribute is what exposes aria-readonly — do not duplicate it. -->\n <textarea\n textarea-control\n rows=\"1\"\n [id]=\"fieldId(category, key)\"\n [formField]=\"field\"\n [attr.placeholder]=\"'userProfile.notDefined' | transloco\"\n [attr.aria-invalid]=\"invalid ? 'true' : null\"\n [attr.aria-describedby]=\"\n invalid ? errorId(category, key) : null\n \"\n [attr.aria-keyshortcuts]=\"editing ? 'Escape' : null\"\n (focus)=\"onFieldFocus($event)\"\n (keydown.escape)=\"onCancelEdit(category, key)\"></textarea>\n </textarea-group>\n </div>\n <!-- group-focus-within, and not only group-hover: `invisible` takes the buttons out of\n the tab order, so without it editing or deleting a property is unreachable with a\n keyboard. Focusing the field of the row reveals its actions. -->\n @if (editing) {\n <button\n type=\"submit\"\n variant=\"tertiary\"\n [iconOnly]=\"true\"\n class=\"visible mx-2 cursor-pointer sm:invisible sm:group-focus-within:visible sm:group-hover:visible\"\n [disabled]=\"profileForm().submitting()\"\n [title]=\"'userProfile.saveProperty' | transloco\"\n [attr.aria-label]=\"\n 'userProfile.savePropertyNamed'\n | transloco: { property: label }\n \">\n <save-icon />\n </button>\n <button\n type=\"button\"\n variant=\"tertiary\"\n [iconOnly]=\"true\"\n class=\"visible cursor-pointer sm:invisible sm:group-focus-within:visible sm:group-hover:visible\"\n [title]=\"'userProfile.cancelEdit' | transloco\"\n [attr.aria-label]=\"\n 'userProfile.cancelEditNamed'\n | transloco: { property: label }\n \"\n (click)=\"onCancelEdit(category, key)\">\n <UndoIcon />\n </button>\n } @else {\n <button\n type=\"button\"\n variant=\"tertiary\"\n [iconOnly]=\"true\"\n [id]=\"editButtonId(category, key)\"\n class=\"visible mx-2 cursor-pointer sm:invisible sm:group-focus-within:visible sm:group-hover:visible\"\n [title]=\"'userProfile.editProperty' | transloco\"\n [attr.aria-label]=\"\n 'userProfile.editPropertyNamed'\n | transloco: { property: label }\n \"\n (click)=\"onEdit(category, key)\">\n <EditIcon />\n </button>\n <button\n type=\"button\"\n variant=\"tertiary\"\n [iconOnly]=\"true\"\n class=\"text-destructive visible cursor-pointer sm:invisible sm:group-focus-within:visible sm:group-hover:visible\"\n [title]=\"'userProfile.deleteProperty' | transloco\"\n [attr.aria-label]=\"\n 'userProfile.deletePropertyNamed'\n | transloco: { property: label }\n \"\n (click)=\"onDeleteData(category, key)\">\n <TrashIcon />\n </button>\n }\n </div>\n @if (invalid) {\n <p\n [id]=\"errorId(category, key)\"\n role=\"alert\"\n [class]=\"errorClass\">\n {{ errorKey(field) | transloco }}\n </p>\n }\n </div>\n }\n </form>\n <!-- Always in the DOM, and only its text changes: a live region inserted at the same time as\n its content is not reliably announced. `sr-only` — the transitions it describes are\n already visible on screen. -->\n <p class=\"sr-only\" role=\"status\" aria-live=\"polite\">\n {{ statusMessage() }}\n </p>\n </div>\n }\n </div>\n} @else if (userProfileResource.isLoading()) {\n <p role=\"status\">{{ \"userProfile.loading\" | transloco }}</p>\n} @else if (userProfileResource.error()) {\n <p role=\"alert\">{{ \"userProfile.loadError\" | transloco }}</p>\n}\n\n<!-- AVATAR UPLOAD — `hidden` keeps it out of both the tab order and the accessibility tree, which is\n intended: it is opened by the avatar button above. It also sits outside the <form>. -->\n<input\n #avatarInput\n class=\"hidden\"\n type=\"file\"\n accept=\"image/*\"\n (change)=\"uploadAvatar($event)\" />\n"
|
|
32578
|
+
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"
|
|
32028
32579
|
}]
|
|
32029
32580
|
}],
|
|
32030
32581
|
ctorParameters: () => []
|
|
32031
32582
|
});
|
|
32032
32583
|
function toRecord(keys, source) {
|
|
32033
|
-
return Object.fromEntries(keys.map((key) => [key,
|
|
32584
|
+
return Object.fromEntries(keys.map((key) => [key, toFieldText(source?.[key])]));
|
|
32034
32585
|
}
|
|
32035
32586
|
function splitPropertyId(id) {
|
|
32036
32587
|
const separator = id.indexOf(".");
|
|
@@ -32672,8 +33223,10 @@ var SettingsDialogComponent = class SettingsDialogComponent {
|
|
|
32672
33223
|
template: `
|
|
32673
33224
|
@if (!duplicate) {
|
|
32674
33225
|
<div dialog #dialog="dialog" [closeLabel]="'settings.close' | transloco" (closed)="call.end($event)">
|
|
32675
|
-
|
|
32676
|
-
|
|
33226
|
+
<!-- The inset layout of the sidebar, in a dialog: the navigation sits on the tinted surface of the
|
|
33227
|
+
dialog itself and the panel is a white card set into it. -->
|
|
33228
|
+
<DialogContent size="xl" class="h-[min(48rem,calc(100dvh-2rem))] flex-row! items-stretch! bg-(--bg-primary-lighter)">
|
|
33229
|
+
<div class="flex w-64 shrink-0 flex-col gap-(--spacing-3xs) p-(--spacing-sm)">
|
|
32677
33230
|
<button
|
|
32678
33231
|
type="button"
|
|
32679
33232
|
variant="none"
|
|
@@ -32710,7 +33263,7 @@ var SettingsDialogComponent = class SettingsDialogComponent {
|
|
|
32710
33263
|
</nav>
|
|
32711
33264
|
</div>
|
|
32712
33265
|
|
|
32713
|
-
<div class="
|
|
33266
|
+
<div sidebarInset class="my-(--spacing-xs) mr-(--spacing-xs) overflow-y-auto rounded-(--radius-2xl) bg-(--bg-neutral-white) p-(--spacing-xl) shadow-none">
|
|
32714
33267
|
@switch (panel()) {
|
|
32715
33268
|
@case ("account") {
|
|
32716
33269
|
<settings-account-panel class="flex min-h-0 flex-1 flex-col" [focusOnOpen]="!navigated()" />
|
|
@@ -32783,6 +33336,12 @@ var SettingsDialogComponent = class SettingsDialogComponent {
|
|
|
32783
33336
|
selector: "x-mark, x-mark-icon, XMark, XMarkIcon",
|
|
32784
33337
|
inputs: ["class"]
|
|
32785
33338
|
},
|
|
33339
|
+
{
|
|
33340
|
+
kind: "directive",
|
|
33341
|
+
type: SidebarInsetDirective,
|
|
33342
|
+
selector: "[sidebarInset]",
|
|
33343
|
+
inputs: ["class"]
|
|
33344
|
+
},
|
|
32786
33345
|
{
|
|
32787
33346
|
kind: "component",
|
|
32788
33347
|
type: SettingsAccountPanelComponent,
|
|
@@ -32825,6 +33384,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
32825
33384
|
PaletteIcon,
|
|
32826
33385
|
KeyboardIcon,
|
|
32827
33386
|
XMarkIcon,
|
|
33387
|
+
SidebarInsetDirective,
|
|
32828
33388
|
SettingsAccountPanelComponent,
|
|
32829
33389
|
SettingsAppearancePanelComponent,
|
|
32830
33390
|
SettingsShortcutsPanelComponent
|
|
@@ -32833,8 +33393,10 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
32833
33393
|
template: `
|
|
32834
33394
|
@if (!duplicate) {
|
|
32835
33395
|
<div dialog #dialog="dialog" [closeLabel]="'settings.close' | transloco" (closed)="call.end($event)">
|
|
32836
|
-
|
|
32837
|
-
|
|
33396
|
+
<!-- The inset layout of the sidebar, in a dialog: the navigation sits on the tinted surface of the
|
|
33397
|
+
dialog itself and the panel is a white card set into it. -->
|
|
33398
|
+
<DialogContent size="xl" class="h-[min(48rem,calc(100dvh-2rem))] flex-row! items-stretch! bg-(--bg-primary-lighter)">
|
|
33399
|
+
<div class="flex w-64 shrink-0 flex-col gap-(--spacing-3xs) p-(--spacing-sm)">
|
|
32838
33400
|
<button
|
|
32839
33401
|
type="button"
|
|
32840
33402
|
variant="none"
|
|
@@ -32871,7 +33433,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
32871
33433
|
</nav>
|
|
32872
33434
|
</div>
|
|
32873
33435
|
|
|
32874
|
-
<div class="
|
|
33436
|
+
<div sidebarInset class="my-(--spacing-xs) mr-(--spacing-xs) overflow-y-auto rounded-(--radius-2xl) bg-(--bg-neutral-white) p-(--spacing-xl) shadow-none">
|
|
32875
33437
|
@switch (panel()) {
|
|
32876
33438
|
@case ("account") {
|
|
32877
33439
|
<settings-account-panel class="flex min-h-0 flex-1 flex-col" [focusOnOpen]="!navigated()" />
|