@sinequa/atomic-angular 2.6.0 → 2.7.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/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/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 +323 -55
- package/fesm2022/sinequa-atomic-angular.mjs.map +1 -1
- package/package.json +1 -1
- package/types/sinequa-atomic-angular-document-workspaces.d.ts +1 -1
- package/types/sinequa-atomic-angular.d.ts +407 -334
|
@@ -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, 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, LockIcon, 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, SidebarInsetDirective, SlashIcon, SlidersIcon, 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, 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";
|
|
@@ -296,8 +296,9 @@ function withAppFeatures() {
|
|
|
296
296
|
};
|
|
297
297
|
},
|
|
298
298
|
getQueryByName(name) {
|
|
299
|
+
if (!name) return void 0;
|
|
299
300
|
const queries = store.queries();
|
|
300
|
-
return Object.values(queries).find((query) => query.name
|
|
301
|
+
return Object.values(queries).find((query) => query.name?.toLowerCase() === name.toLowerCase());
|
|
301
302
|
},
|
|
302
303
|
getQueryByIndex(index) {
|
|
303
304
|
const queries = store.queries();
|
|
@@ -805,6 +806,9 @@ var QueryService = class QueryService {
|
|
|
805
806
|
result = signal({}, ...ngDevMode ? [{ debugName: "result" }] : /* istanbul ignore next */ []);
|
|
806
807
|
failure = computed(() => this.result().$error, ...ngDevMode ? [{ debugName: "failure" }] : /* istanbul ignore next */ []);
|
|
807
808
|
audit;
|
|
809
|
+
warnEmptySearch() {
|
|
810
|
+
firstValueFrom(this.transloco.selectTranslate("emptySearch", {}, "searches")).catch(() => "Please enter a search query.").then((message) => notify.warning(message));
|
|
811
|
+
}
|
|
808
812
|
search(q, includeQueryParams = true, audit) {
|
|
809
813
|
const $auditRecord = { auditEvents: [audit ?? this.audit ?? {
|
|
810
814
|
type: "Search_Text",
|
|
@@ -820,7 +824,7 @@ var QueryService = class QueryService {
|
|
|
820
824
|
if (!((this.queryParamsStore.filters?.() || []).length > 0) && !query.basket && query.action !== "open") {
|
|
821
825
|
if (this.appStore.allowEmptySearch(query?.name || "") === false && query?.text === "") {
|
|
822
826
|
warn("QueryService.search: Empty search query is not allowed. Returning empty result.");
|
|
823
|
-
|
|
827
|
+
this.warnEmptySearch();
|
|
824
828
|
const emptyResult = { records: [] };
|
|
825
829
|
this.result.set(emptyResult);
|
|
826
830
|
return of(emptyResult);
|
|
@@ -3243,11 +3247,13 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
3243
3247
|
});
|
|
3244
3248
|
var UserProfileService = class UserProfileService {
|
|
3245
3249
|
client = inject(ATOMIC_CLIENT);
|
|
3250
|
+
injector = inject(Injector);
|
|
3246
3251
|
resources = /* @__PURE__ */ new WeakMap();
|
|
3247
3252
|
getUserProfile(userId) {
|
|
3248
3253
|
let resourceRef = this.resources.get(userId);
|
|
3249
3254
|
if (!resourceRef) {
|
|
3250
3255
|
resourceRef = resource({
|
|
3256
|
+
injector: this.injector,
|
|
3251
3257
|
params: () => {
|
|
3252
3258
|
const id = userId();
|
|
3253
3259
|
return id ? { id } : void 0;
|
|
@@ -3426,17 +3432,61 @@ const QUERY_PARAMS_DEFAULTS = {
|
|
|
3426
3432
|
neuralSearch: void 0,
|
|
3427
3433
|
aggregation_overrides: void 0
|
|
3428
3434
|
};
|
|
3435
|
+
function parseHref(href) {
|
|
3436
|
+
let url;
|
|
3437
|
+
try {
|
|
3438
|
+
url = new URL(href, window.location.origin);
|
|
3439
|
+
} catch {
|
|
3440
|
+
return {
|
|
3441
|
+
path: "",
|
|
3442
|
+
params: new URLSearchParams()
|
|
3443
|
+
};
|
|
3444
|
+
}
|
|
3445
|
+
if (url.hash.startsWith("#/")) {
|
|
3446
|
+
const route = url.hash.slice(1);
|
|
3447
|
+
const queryIndex = route.indexOf("?");
|
|
3448
|
+
const path = queryIndex === -1 ? route : route.slice(0, queryIndex);
|
|
3449
|
+
const search = queryIndex === -1 ? "" : route.slice(queryIndex + 1);
|
|
3450
|
+
return {
|
|
3451
|
+
path,
|
|
3452
|
+
params: new URLSearchParams(search)
|
|
3453
|
+
};
|
|
3454
|
+
}
|
|
3455
|
+
return {
|
|
3456
|
+
path: "",
|
|
3457
|
+
params: url.searchParams
|
|
3458
|
+
};
|
|
3459
|
+
}
|
|
3460
|
+
function parseFilters(raw) {
|
|
3461
|
+
if (!raw) return [];
|
|
3462
|
+
const candidates = [raw];
|
|
3463
|
+
try {
|
|
3464
|
+
const decoded = decodeURIComponent(raw);
|
|
3465
|
+
if (decoded !== raw) candidates.push(decoded);
|
|
3466
|
+
} catch {}
|
|
3467
|
+
for (const candidate of candidates) try {
|
|
3468
|
+
const parsed = JSON.parse(candidate);
|
|
3469
|
+
if (Array.isArray(parsed)) return parsed;
|
|
3470
|
+
} catch {}
|
|
3471
|
+
return [];
|
|
3472
|
+
}
|
|
3429
3473
|
const QueryParamsStore = signalStore({ providedIn: "root" }, withDevtools("QueryParams"), withQueryParamsFeatures());
|
|
3430
3474
|
function withQueryParamsFeatures() {
|
|
3431
3475
|
return signalStoreFeature(withState(QUERY_PARAMS_DEFAULTS), withMethods((store, injector = inject(Injector), router = inject(Router), route = inject(ActivatedRoute), appStore = inject(AppStore)) => ({
|
|
3432
3476
|
setFromUrl(href) {
|
|
3433
3477
|
if (!href) return;
|
|
3434
|
-
const path =
|
|
3435
|
-
const
|
|
3436
|
-
const filters =
|
|
3437
|
-
const
|
|
3438
|
-
|
|
3439
|
-
|
|
3478
|
+
const { path, params } = parseHref(href);
|
|
3479
|
+
const text = params.get("q") ?? "";
|
|
3480
|
+
const filters = parseFilters(params.get("f"));
|
|
3481
|
+
const id = params.get("id") ?? "";
|
|
3482
|
+
const sort = params.get("s") ?? "";
|
|
3483
|
+
const tab = params.get("t") ?? "";
|
|
3484
|
+
const basket = params.get("b") ?? "";
|
|
3485
|
+
const name = params.get("n") ?? "";
|
|
3486
|
+
const spellingCorrectionMode = params.get("c") || void 0;
|
|
3487
|
+
const p = params.get("p");
|
|
3488
|
+
const parsedPage = p ? parseInt(p, 10) : NaN;
|
|
3489
|
+
const page = Number.isNaN(parsedPage) ? void 0 : parsedPage;
|
|
3440
3490
|
patchState(store, (state) => {
|
|
3441
3491
|
return {
|
|
3442
3492
|
...state,
|
|
@@ -4934,14 +4984,15 @@ var LoadingComponent = class LoadingComponent {
|
|
|
4934
4984
|
clearTimeout(timeout);
|
|
4935
4985
|
setTimeout(() => {
|
|
4936
4986
|
const url = this.route.snapshot.queryParams["returnUrl"] || null;
|
|
4937
|
-
if (
|
|
4938
|
-
this.router.navigateByUrl(url);
|
|
4987
|
+
if (url) {
|
|
4939
4988
|
this.queryParamsStore.setFromUrl(url);
|
|
4989
|
+
this.router.navigateByUrl(url);
|
|
4940
4990
|
} else this.router.navigate(["/"]);
|
|
4941
4991
|
}, 700);
|
|
4942
4992
|
}
|
|
4943
4993
|
});
|
|
4944
|
-
if (
|
|
4994
|
+
if (this.client.auth.isAuthenticated()) this.application.updateReadyState(true);
|
|
4995
|
+
else {
|
|
4945
4996
|
this.application.updateReadyState(false);
|
|
4946
4997
|
this.client.auth.login().then(() => {
|
|
4947
4998
|
this.application.updateReadyState(true);
|
|
@@ -7203,17 +7254,18 @@ var InfinityScrollDirective = class InfinityScrollDirective {
|
|
|
7203
7254
|
alias: "scroll-options"
|
|
7204
7255
|
});
|
|
7205
7256
|
onScroll = output();
|
|
7206
|
-
observer
|
|
7207
|
-
if (entry.isIntersecting) this.onScroll.emit();
|
|
7208
|
-
}, this.options());
|
|
7257
|
+
observer;
|
|
7209
7258
|
el = inject(ElementRef);
|
|
7210
7259
|
constructor() {
|
|
7211
7260
|
afterNextRender(() => {
|
|
7261
|
+
this.observer = new IntersectionObserver(([entry]) => {
|
|
7262
|
+
if (entry.isIntersecting) this.onScroll.emit();
|
|
7263
|
+
}, this.options());
|
|
7212
7264
|
this.observer.observe(this.el.nativeElement);
|
|
7213
7265
|
});
|
|
7214
7266
|
}
|
|
7215
7267
|
ngOnDestroy() {
|
|
7216
|
-
this.observer
|
|
7268
|
+
this.observer?.disconnect();
|
|
7217
7269
|
}
|
|
7218
7270
|
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
7219
7271
|
minVersion: "12.0.0",
|
|
@@ -18692,7 +18744,6 @@ var BookmarkButtonComponent = class BookmarkButtonComponent {
|
|
|
18692
18744
|
isBookmarked = computed(() => {
|
|
18693
18745
|
return this.userSettingsStore.isBookmarked(this.article());
|
|
18694
18746
|
}, ...ngDevMode ? [{ debugName: "isBookmarked" }] : /* istanbul ignore next */ []);
|
|
18695
|
-
title = computed(() => this.transloco.translate("bookmarks.bookmarkDocument"), ...ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []);
|
|
18696
18747
|
async bookmark(e) {
|
|
18697
18748
|
e.stopPropagation();
|
|
18698
18749
|
const article = this.article();
|
|
@@ -18756,11 +18807,11 @@ var BookmarkButtonComponent = class BookmarkButtonComponent {
|
|
|
18756
18807
|
properties: { "class.visible": "isBookmarked()" },
|
|
18757
18808
|
classAttribute: "cursor-pointer"
|
|
18758
18809
|
},
|
|
18759
|
-
providers: [provideTranslocoScope("
|
|
18810
|
+
providers: [provideTranslocoScope("bookmarks")],
|
|
18760
18811
|
ngImport: i0,
|
|
18761
18812
|
template: `
|
|
18762
18813
|
<button [variant]="variant()" [size]="size()" [iconOnly]="true"
|
|
18763
|
-
[tooltip]="
|
|
18814
|
+
[tooltip]="'bookmarks.bookmarkDocument' | transloco"
|
|
18764
18815
|
[attr.aria-pressed]="isBookmarked()"
|
|
18765
18816
|
[attr.tabindex]="tabIndex()"
|
|
18766
18817
|
(click)="bookmark($event)">
|
|
@@ -18770,7 +18821,7 @@ var BookmarkButtonComponent = class BookmarkButtonComponent {
|
|
|
18770
18821
|
@else {
|
|
18771
18822
|
<bookmark-icon />
|
|
18772
18823
|
}
|
|
18773
|
-
<span class="sr-only">{{
|
|
18824
|
+
<span class="sr-only">{{ 'bookmarks.bookmarkDocument' | transloco }}</span>
|
|
18774
18825
|
</button>
|
|
18775
18826
|
`,
|
|
18776
18827
|
isInline: true,
|
|
@@ -18818,6 +18869,11 @@ var BookmarkButtonComponent = class BookmarkButtonComponent {
|
|
|
18818
18869
|
"tooltip-offset"
|
|
18819
18870
|
],
|
|
18820
18871
|
exportAs: ["tooltip"]
|
|
18872
|
+
},
|
|
18873
|
+
{
|
|
18874
|
+
kind: "pipe",
|
|
18875
|
+
type: TranslocoPipe,
|
|
18876
|
+
name: "transloco"
|
|
18821
18877
|
}
|
|
18822
18878
|
]
|
|
18823
18879
|
});
|
|
@@ -18834,12 +18890,13 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
18834
18890
|
imports: [
|
|
18835
18891
|
BookmarkIcon,
|
|
18836
18892
|
ButtonComponent,
|
|
18837
|
-
TooltipDirective
|
|
18893
|
+
TooltipDirective,
|
|
18894
|
+
TranslocoPipe
|
|
18838
18895
|
],
|
|
18839
|
-
providers: [provideTranslocoScope("
|
|
18896
|
+
providers: [provideTranslocoScope("bookmarks")],
|
|
18840
18897
|
template: `
|
|
18841
18898
|
<button [variant]="variant()" [size]="size()" [iconOnly]="true"
|
|
18842
|
-
[tooltip]="
|
|
18899
|
+
[tooltip]="'bookmarks.bookmarkDocument' | transloco"
|
|
18843
18900
|
[attr.aria-pressed]="isBookmarked()"
|
|
18844
18901
|
[attr.tabindex]="tabIndex()"
|
|
18845
18902
|
(click)="bookmark($event)">
|
|
@@ -18849,7 +18906,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
18849
18906
|
@else {
|
|
18850
18907
|
<bookmark-icon />
|
|
18851
18908
|
}
|
|
18852
|
-
<span class="sr-only">{{
|
|
18909
|
+
<span class="sr-only">{{ 'bookmarks.bookmarkDocument' | transloco }}</span>
|
|
18853
18910
|
</button>
|
|
18854
18911
|
`,
|
|
18855
18912
|
host: {
|
|
@@ -21890,7 +21947,7 @@ var DidYouMeanComponent = class DidYouMeanComponent {
|
|
|
21890
21947
|
<p>
|
|
21891
21948
|
<span class="me-1">{{ 'didYouMean.didYouMean' | transloco }}</span>
|
|
21892
21949
|
<a class="text-(--font-primary-base) hover:underline cursor-pointer" role="button" (click)="selectCorrected()">{{ correction() }}</a>
|
|
21893
|
-
<span
|
|
21950
|
+
<span>{{ 'didYouMean.didYouMeanSuffix' | transloco }}</span>
|
|
21894
21951
|
</p>
|
|
21895
21952
|
}
|
|
21896
21953
|
@case ('correction') {
|
|
@@ -21940,7 +21997,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
21940
21997
|
<p>
|
|
21941
21998
|
<span class="me-1">{{ 'didYouMean.didYouMean' | transloco }}</span>
|
|
21942
21999
|
<a class="text-(--font-primary-base) hover:underline cursor-pointer" role="button" (click)="selectCorrected()">{{ correction() }}</a>
|
|
21943
|
-
<span
|
|
22000
|
+
<span>{{ 'didYouMean.didYouMeanSuffix' | transloco }}</span>
|
|
21944
22001
|
</p>
|
|
21945
22002
|
}
|
|
21946
22003
|
@case ('correction') {
|
|
@@ -31619,6 +31676,36 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
31619
31676
|
}]
|
|
31620
31677
|
}] }
|
|
31621
31678
|
});
|
|
31679
|
+
const isPlainObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
31680
|
+
const LEAF_SEPARATOR = " › ";
|
|
31681
|
+
function flattenLeaves(source, prefix = []) {
|
|
31682
|
+
return Object.entries(source).flatMap(([name, value]) => isPlainObject(value) ? flattenLeaves(value, [...prefix, name]) : [{
|
|
31683
|
+
path: [...prefix, name],
|
|
31684
|
+
value
|
|
31685
|
+
}]);
|
|
31686
|
+
}
|
|
31687
|
+
function valueAtPath(source, path) {
|
|
31688
|
+
return path.reduce((node, name) => isPlainObject(node) ? node[name] : void 0, source);
|
|
31689
|
+
}
|
|
31690
|
+
const toFieldText = (value) => {
|
|
31691
|
+
if (value === void 0 || value === null) return "";
|
|
31692
|
+
return Array.isArray(value) ? JSON.stringify(value) : String(value);
|
|
31693
|
+
};
|
|
31694
|
+
function normalizeCustomData(entries) {
|
|
31695
|
+
const properties = /* @__PURE__ */ new Map();
|
|
31696
|
+
const add = (key, value) => {
|
|
31697
|
+
if (!key || properties.has(key)) return;
|
|
31698
|
+
properties.set(key, value === void 0 || value === null ? { key } : {
|
|
31699
|
+
key,
|
|
31700
|
+
defaultValue: value
|
|
31701
|
+
});
|
|
31702
|
+
};
|
|
31703
|
+
for (const entry of Array.isArray(entries) ? entries : []) if (typeof entry === "string") add(entry);
|
|
31704
|
+
else if (entry && typeof entry === "object" && !Array.isArray(entry)) Object.entries(entry).forEach(([key, value]) => add(key, value));
|
|
31705
|
+
else console.warn("[user-profile] customData: entry ignored, expected a string or an object.", entry);
|
|
31706
|
+
return [...properties.values()];
|
|
31707
|
+
}
|
|
31708
|
+
const SAVED_FLASH_MS = 3e3;
|
|
31622
31709
|
const sameKeys = (a, b) => a.length === b.length && a.every((key, index) => key === b[index]);
|
|
31623
31710
|
const userProfileSchema = (editing) => schema((p) => {
|
|
31624
31711
|
applyEach(p.data, (field) => readonly(field, { when: ({ key }) => `data.${key()}` !== editing() }));
|
|
@@ -31632,9 +31719,13 @@ var UserProfileFormComponent = class UserProfileFormComponent {
|
|
|
31632
31719
|
transloco = inject(TranslocoService);
|
|
31633
31720
|
injector = inject(Injector);
|
|
31634
31721
|
document = inject(DOCUMENT);
|
|
31722
|
+
destroyRef = inject(DestroyRef);
|
|
31635
31723
|
labelClass = FORM_LABEL_CLASS;
|
|
31636
31724
|
errorClass = FORM_ERROR_CLASS;
|
|
31725
|
+
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
31726
|
statusMessage = signal("", ...ngDevMode ? [{ debugName: "statusMessage" }] : /* istanbul ignore next */ []);
|
|
31727
|
+
savedProperty = signal(void 0, ...ngDevMode ? [{ debugName: "savedProperty" }] : /* istanbul ignore next */ []);
|
|
31728
|
+
savedTimer;
|
|
31638
31729
|
dataKeys = [
|
|
31639
31730
|
"fullName",
|
|
31640
31731
|
"mail",
|
|
@@ -31657,10 +31748,29 @@ var UserProfileFormComponent = class UserProfileFormComponent {
|
|
|
31657
31748
|
equal: sameKeys
|
|
31658
31749
|
});
|
|
31659
31750
|
allowProfilePhoto = computed(() => this.keys().some((key) => key === "profilePhoto"), ...ngDevMode ? [{ debugName: "allowProfilePhoto" }] : /* istanbul ignore next */ []);
|
|
31660
|
-
|
|
31751
|
+
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 */ []);
|
|
31752
|
+
customRows = computed(() => {
|
|
31753
|
+
const stored = this.user()?.customData;
|
|
31754
|
+
return this.customDataConfig().flatMap(({ key, defaultValue }) => {
|
|
31755
|
+
const shape = isPlainObject(defaultValue) ? defaultValue : isPlainObject(stored?.[key]) ? stored[key] : void 0;
|
|
31756
|
+
if (!shape) return [{
|
|
31757
|
+
key,
|
|
31758
|
+
root: key,
|
|
31759
|
+
defaultValue
|
|
31760
|
+
}];
|
|
31761
|
+
return flattenLeaves(shape).map(({ path, value }) => ({
|
|
31762
|
+
key: [key, ...path].join(LEAF_SEPARATOR),
|
|
31763
|
+
root: key,
|
|
31764
|
+
path: [key, ...path],
|
|
31765
|
+
defaultValue: isPlainObject(defaultValue) ? value : void 0
|
|
31766
|
+
}));
|
|
31767
|
+
});
|
|
31768
|
+
}, ...ngDevMode ? [{ debugName: "customRows" }] : /* istanbul ignore next */ []);
|
|
31769
|
+
customData = computed(() => this.customRows().map(({ key }) => key), {
|
|
31661
31770
|
...ngDevMode ? { debugName: "customData" } : /* istanbul ignore next */ {},
|
|
31662
31771
|
equal: sameKeys
|
|
31663
31772
|
});
|
|
31773
|
+
customRowIndex = computed(() => new Map(this.customRows().map((row) => [row.key, row])), ...ngDevMode ? [{ debugName: "customRowIndex" }] : /* istanbul ignore next */ []);
|
|
31664
31774
|
properties = computed(() => [...this.formKeys().map((key) => ({
|
|
31665
31775
|
category: "data",
|
|
31666
31776
|
key,
|
|
@@ -31670,6 +31780,13 @@ var UserProfileFormComponent = class UserProfileFormComponent {
|
|
|
31670
31780
|
key,
|
|
31671
31781
|
translated: false
|
|
31672
31782
|
}))], ...ngDevMode ? [{ debugName: "properties" }] : /* istanbul ignore next */ []);
|
|
31783
|
+
sections = computed(() => {
|
|
31784
|
+
const rows = this.properties();
|
|
31785
|
+
return ["data", "customData"].map((category) => ({
|
|
31786
|
+
id: category === "data" ? "information" : "preferences",
|
|
31787
|
+
rows: rows.filter((row) => row.category === category)
|
|
31788
|
+
})).filter((section) => section.rows.length > 0);
|
|
31789
|
+
}, ...ngDevMode ? [{ debugName: "sections" }] : /* istanbul ignore next */ []);
|
|
31673
31790
|
initials = computed(() => this.principalStore.initials(), ...ngDevMode ? [{ debugName: "initials" }] : /* istanbul ignore next */ []);
|
|
31674
31791
|
username = computed(() => this.principalStore.name() || this.principalStore.email() || null, ...ngDevMode ? [{ debugName: "username" }] : /* istanbul ignore next */ []);
|
|
31675
31792
|
propertyToEdit = signal(void 0, ...ngDevMode ? [{ debugName: "propertyToEdit" }] : /* istanbul ignore next */ []);
|
|
@@ -31695,7 +31812,7 @@ var UserProfileFormComponent = class UserProfileFormComponent {
|
|
|
31695
31812
|
const profile = this.user();
|
|
31696
31813
|
return {
|
|
31697
31814
|
data: toRecord(this.formKeys(), profile?.data),
|
|
31698
|
-
customData:
|
|
31815
|
+
customData: Object.fromEntries(this.customData().map((key) => [key, toFieldText(this.effectiveValue("customData", key))]))
|
|
31699
31816
|
};
|
|
31700
31817
|
}, ...ngDevMode ? [{ debugName: "profileModel" }] : /* istanbul ignore next */ []);
|
|
31701
31818
|
profileForm = form(this.profileModel, userProfileSchema(this.propertyToEdit), {
|
|
@@ -31706,6 +31823,7 @@ var UserProfileFormComponent = class UserProfileFormComponent {
|
|
|
31706
31823
|
effect(() => {
|
|
31707
31824
|
if (this.userProfileResource.error()?.status === 404) this.createUserProfile();
|
|
31708
31825
|
});
|
|
31826
|
+
this.destroyRef.onDestroy(() => clearTimeout(this.savedTimer));
|
|
31709
31827
|
}
|
|
31710
31828
|
fieldId(category, key) {
|
|
31711
31829
|
return `user-profile-${category}-${key.replace(/[^\w-]+/g, "-")}`;
|
|
@@ -31734,6 +31852,33 @@ var UserProfileFormComponent = class UserProfileFormComponent {
|
|
|
31734
31852
|
getDataValue(category, key) {
|
|
31735
31853
|
return this.user()?.[category]?.[key];
|
|
31736
31854
|
}
|
|
31855
|
+
defaultValue(category, key) {
|
|
31856
|
+
return category === "customData" ? this.customRowIndex().get(key)?.defaultValue : void 0;
|
|
31857
|
+
}
|
|
31858
|
+
isLeaf(category, key) {
|
|
31859
|
+
return category === "customData" && this.customRowIndex().get(key)?.path !== void 0;
|
|
31860
|
+
}
|
|
31861
|
+
storedValue(category, key) {
|
|
31862
|
+
const bucket = this.user()?.[category];
|
|
31863
|
+
const path = category === "customData" ? this.customRowIndex().get(key)?.path : void 0;
|
|
31864
|
+
return path ? valueAtPath(bucket, path) : bucket?.[key];
|
|
31865
|
+
}
|
|
31866
|
+
isBoolean(category, key) {
|
|
31867
|
+
return !this.isLeaf(category, key) && typeof this.effectiveValue(category, key) === "boolean";
|
|
31868
|
+
}
|
|
31869
|
+
booleanValue(category, key) {
|
|
31870
|
+
return this.effectiveValue(category, key) === true;
|
|
31871
|
+
}
|
|
31872
|
+
isReadOnlyValue(category, key) {
|
|
31873
|
+
return Array.isArray(this.effectiveValue(category, key));
|
|
31874
|
+
}
|
|
31875
|
+
arrayTags(category, key) {
|
|
31876
|
+
const value = this.effectiveValue(category, key);
|
|
31877
|
+
return [...new Set((Array.isArray(value) ? value : []).map((item) => typeof item === "string" ? item : JSON.stringify(item)))];
|
|
31878
|
+
}
|
|
31879
|
+
effectiveValue(category, key) {
|
|
31880
|
+
return this.storedValue(category, key) ?? this.defaultValue(category, key);
|
|
31881
|
+
}
|
|
31737
31882
|
onEdit(category, key) {
|
|
31738
31883
|
this.propertyToEdit.set(`${category}.${key}`);
|
|
31739
31884
|
this.fieldFor(category, key)?.().focusBoundControl();
|
|
@@ -31743,19 +31888,43 @@ var UserProfileFormComponent = class UserProfileFormComponent {
|
|
|
31743
31888
|
const field = event.target;
|
|
31744
31889
|
field.setSelectionRange(field.value.length, field.value.length);
|
|
31745
31890
|
}
|
|
31891
|
+
onEscape(event, category, key) {
|
|
31892
|
+
if (!this.isEditing(category, key)) return;
|
|
31893
|
+
event.preventDefault();
|
|
31894
|
+
event.stopPropagation();
|
|
31895
|
+
this.onCancelEdit(category, key);
|
|
31896
|
+
}
|
|
31746
31897
|
onCancelEdit(category, key) {
|
|
31747
31898
|
if (!this.isEditing(category, key)) return;
|
|
31748
31899
|
const state = this.fieldFor(category, key)?.();
|
|
31749
|
-
state?.value.set(this.
|
|
31900
|
+
state?.value.set(toFieldText(this.effectiveValue(category, key)));
|
|
31750
31901
|
state?.reset();
|
|
31751
31902
|
this.leaveEditMode(category, key);
|
|
31752
31903
|
this.announce("editCancelled", category, key);
|
|
31753
31904
|
}
|
|
31754
|
-
|
|
31755
|
-
|
|
31905
|
+
labelFor(category, key) {
|
|
31906
|
+
if (category === "data") return this.transloco.translate(`userProfile.data.${key}`);
|
|
31907
|
+
const leafPath = this.customRowIndex().get(key)?.path;
|
|
31908
|
+
if (leafPath) return [this.labelFor(category, leafPath[0]), ...leafPath.slice(1)].join(LEAF_SEPARATOR);
|
|
31909
|
+
const path = `userProfile.customData.${key}`;
|
|
31910
|
+
const translated = this.transloco.translate(path);
|
|
31911
|
+
return translated && translated !== path ? translated : key;
|
|
31912
|
+
}
|
|
31913
|
+
isDefault(category, key) {
|
|
31914
|
+
const stored = this.storedValue(category, key);
|
|
31915
|
+
return (stored === void 0 || stored === null) && this.defaultValue(category, key) !== void 0;
|
|
31916
|
+
}
|
|
31917
|
+
isSaved(category, key) {
|
|
31918
|
+
return this.savedProperty() === `${category}.${key}`;
|
|
31919
|
+
}
|
|
31920
|
+
flashSaved(category, key) {
|
|
31921
|
+
this.savedProperty.set(`${category}.${key}`);
|
|
31922
|
+
clearTimeout(this.savedTimer);
|
|
31923
|
+
this.savedTimer = setTimeout(() => this.savedProperty.set(void 0), SAVED_FLASH_MS);
|
|
31756
31924
|
}
|
|
31757
31925
|
announce(kind, category, key) {
|
|
31758
|
-
this.statusMessage.set(this.transloco.translate(`userProfile.status.${kind}`, { property: this.
|
|
31926
|
+
this.statusMessage.set(this.transloco.translate(`userProfile.status.${kind}`, { property: this.labelFor(category, key) }));
|
|
31927
|
+
if (kind === "saved") this.flashSaved(category, key);
|
|
31759
31928
|
}
|
|
31760
31929
|
leaveEditMode(category, key) {
|
|
31761
31930
|
this.propertyToEdit.set(void 0);
|
|
@@ -31789,6 +31958,7 @@ var UserProfileFormComponent = class UserProfileFormComponent {
|
|
|
31789
31958
|
}
|
|
31790
31959
|
}
|
|
31791
31960
|
async onDeleteData(category, key) {
|
|
31961
|
+
if (!this.hasStoredValue(category, key)) return;
|
|
31792
31962
|
try {
|
|
31793
31963
|
const response = await this.client.api.userProfile.deleteProperty(this.user(), category, key);
|
|
31794
31964
|
if (!response) {
|
|
@@ -31796,14 +31966,46 @@ var UserProfileFormComponent = class UserProfileFormComponent {
|
|
|
31796
31966
|
return;
|
|
31797
31967
|
}
|
|
31798
31968
|
info("Deleted user profile property", response);
|
|
31799
|
-
|
|
31800
|
-
userProfile[category][key] = "";
|
|
31801
|
-
this.userProfileResource.set(userProfile);
|
|
31802
|
-
this.announce("deleted", category, key);
|
|
31969
|
+
this.forgetProperty(category, key);
|
|
31803
31970
|
} catch (err) {
|
|
31971
|
+
if (err?.status === 404) {
|
|
31972
|
+
info("User profile property already absent", {
|
|
31973
|
+
category,
|
|
31974
|
+
key
|
|
31975
|
+
});
|
|
31976
|
+
this.forgetProperty(category, key);
|
|
31977
|
+
return;
|
|
31978
|
+
}
|
|
31804
31979
|
notify.error(err.message);
|
|
31805
31980
|
}
|
|
31806
31981
|
}
|
|
31982
|
+
hasStoredValue(category, key) {
|
|
31983
|
+
const stored = this.storedValue(category, key);
|
|
31984
|
+
return stored !== void 0 && stored !== null;
|
|
31985
|
+
}
|
|
31986
|
+
forgetProperty(category, key) {
|
|
31987
|
+
const userProfile = JSON.parse(JSON.stringify(this.user()));
|
|
31988
|
+
if (this.defaultValue(category, key) === void 0) userProfile[category][key] = "";
|
|
31989
|
+
else delete userProfile[category][key];
|
|
31990
|
+
this.userProfileResource.set(userProfile);
|
|
31991
|
+
this.announce("deleted", category, key);
|
|
31992
|
+
}
|
|
31993
|
+
async onToggleBoolean(category, key, checked, toggle) {
|
|
31994
|
+
const previous = this.user();
|
|
31995
|
+
if (!previous || this.booleanValue(category, key) === checked) return;
|
|
31996
|
+
this.userProfileResource.set({
|
|
31997
|
+
...previous,
|
|
31998
|
+
[category]: {
|
|
31999
|
+
...previous[category],
|
|
32000
|
+
[key]: checked
|
|
32001
|
+
}
|
|
32002
|
+
});
|
|
32003
|
+
if (await this.patchProperty(category, key, checked)) this.announce("saved", category, key);
|
|
32004
|
+
else {
|
|
32005
|
+
this.userProfileResource.set(previous);
|
|
32006
|
+
toggle.checked.set(!checked);
|
|
32007
|
+
}
|
|
32008
|
+
}
|
|
31807
32009
|
async patchProperty(category, key, value) {
|
|
31808
32010
|
const userProfile = JSON.parse(JSON.stringify(this.user()));
|
|
31809
32011
|
userProfile[category] = {
|
|
@@ -31857,7 +32059,7 @@ var UserProfileFormComponent = class UserProfileFormComponent {
|
|
|
31857
32059
|
host: { classAttribute: "flex flex-col h-full overflow-hidden" },
|
|
31858
32060
|
providers: [provideTranslocoScope("user-profile", "login")],
|
|
31859
32061
|
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",
|
|
32062
|
+
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
32063
|
dependencies: [
|
|
31862
32064
|
{
|
|
31863
32065
|
kind: "directive",
|
|
@@ -31950,24 +32152,75 @@ var UserProfileFormComponent = class UserProfileFormComponent {
|
|
|
31950
32152
|
"size"
|
|
31951
32153
|
]
|
|
31952
32154
|
},
|
|
32155
|
+
{
|
|
32156
|
+
kind: "component",
|
|
32157
|
+
type: CircleCheckIcon,
|
|
32158
|
+
selector: "circle-check, circle-check-icon, CircleCheck, CircleCheckIcon",
|
|
32159
|
+
inputs: ["class"]
|
|
32160
|
+
},
|
|
32161
|
+
{
|
|
32162
|
+
kind: "component",
|
|
32163
|
+
type: LockIcon,
|
|
32164
|
+
selector: "lock, lock-icon, Lock, LockIcon",
|
|
32165
|
+
inputs: ["class"]
|
|
32166
|
+
},
|
|
32167
|
+
{
|
|
32168
|
+
kind: "component",
|
|
32169
|
+
type: SwitchComponent,
|
|
32170
|
+
selector: "switch, Switch",
|
|
32171
|
+
inputs: [
|
|
32172
|
+
"checked",
|
|
32173
|
+
"disabled",
|
|
32174
|
+
"size",
|
|
32175
|
+
"class",
|
|
32176
|
+
"aria-label",
|
|
32177
|
+
"aria-labelledby",
|
|
32178
|
+
"presentational",
|
|
32179
|
+
"compact",
|
|
32180
|
+
"tabIndex"
|
|
32181
|
+
],
|
|
32182
|
+
outputs: ["checkedChange"]
|
|
32183
|
+
},
|
|
32184
|
+
{
|
|
32185
|
+
kind: "component",
|
|
32186
|
+
type: TagInputComponent,
|
|
32187
|
+
selector: "TagInput, tag-input, taginput",
|
|
32188
|
+
inputs: [
|
|
32189
|
+
"value",
|
|
32190
|
+
"disabled",
|
|
32191
|
+
"readonly",
|
|
32192
|
+
"placeholder",
|
|
32193
|
+
"separator",
|
|
32194
|
+
"addOnBlur",
|
|
32195
|
+
"size",
|
|
32196
|
+
"scheme",
|
|
32197
|
+
"class",
|
|
32198
|
+
"aria-label",
|
|
32199
|
+
"addLabel",
|
|
32200
|
+
"removeLabel",
|
|
32201
|
+
"addedLabel",
|
|
32202
|
+
"removedLabel"
|
|
32203
|
+
],
|
|
32204
|
+
outputs: ["valueChange", "touch"]
|
|
32205
|
+
},
|
|
31953
32206
|
{
|
|
31954
32207
|
kind: "directive",
|
|
31955
|
-
type:
|
|
31956
|
-
selector: "
|
|
32208
|
+
type: InputComponent,
|
|
32209
|
+
selector: "input-group, inputgroup, InputGroup",
|
|
31957
32210
|
inputs: [
|
|
31958
32211
|
"class",
|
|
31959
32212
|
"size",
|
|
31960
32213
|
"scheme",
|
|
31961
|
-
"resize",
|
|
31962
32214
|
"filled",
|
|
31963
|
-
"
|
|
31964
|
-
"
|
|
32215
|
+
"numbers",
|
|
32216
|
+
"autoHeight",
|
|
32217
|
+
"textSize"
|
|
31965
32218
|
]
|
|
31966
32219
|
},
|
|
31967
32220
|
{
|
|
31968
32221
|
kind: "directive",
|
|
31969
|
-
type:
|
|
31970
|
-
selector: "
|
|
32222
|
+
type: InputControlDirective,
|
|
32223
|
+
selector: "input[input-control], input[inputControl]",
|
|
31971
32224
|
inputs: ["class"]
|
|
31972
32225
|
},
|
|
31973
32226
|
{
|
|
@@ -32019,18 +32272,22 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
32019
32272
|
UserIcon,
|
|
32020
32273
|
AvatarImageComponent,
|
|
32021
32274
|
ButtonComponent,
|
|
32022
|
-
|
|
32023
|
-
|
|
32275
|
+
CircleCheckIcon,
|
|
32276
|
+
LockIcon,
|
|
32277
|
+
SwitchComponent,
|
|
32278
|
+
TagInputComponent,
|
|
32279
|
+
InputComponent,
|
|
32280
|
+
InputControlDirective,
|
|
32024
32281
|
ChangePasswordComponent
|
|
32025
32282
|
],
|
|
32026
32283
|
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"
|
|
32284
|
+
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
32285
|
}]
|
|
32029
32286
|
}],
|
|
32030
32287
|
ctorParameters: () => []
|
|
32031
32288
|
});
|
|
32032
32289
|
function toRecord(keys, source) {
|
|
32033
|
-
return Object.fromEntries(keys.map((key) => [key,
|
|
32290
|
+
return Object.fromEntries(keys.map((key) => [key, toFieldText(source?.[key])]));
|
|
32034
32291
|
}
|
|
32035
32292
|
function splitPropertyId(id) {
|
|
32036
32293
|
const separator = id.indexOf(".");
|
|
@@ -32672,8 +32929,10 @@ var SettingsDialogComponent = class SettingsDialogComponent {
|
|
|
32672
32929
|
template: `
|
|
32673
32930
|
@if (!duplicate) {
|
|
32674
32931
|
<div dialog #dialog="dialog" [closeLabel]="'settings.close' | transloco" (closed)="call.end($event)">
|
|
32675
|
-
|
|
32676
|
-
|
|
32932
|
+
<!-- The inset layout of the sidebar, in a dialog: the navigation sits on the tinted surface of the
|
|
32933
|
+
dialog itself and the panel is a white card set into it. -->
|
|
32934
|
+
<DialogContent size="xl" class="h-[min(48rem,calc(100dvh-2rem))] flex-row! items-stretch! bg-(--bg-primary-lighter)">
|
|
32935
|
+
<div class="flex w-64 shrink-0 flex-col gap-(--spacing-3xs) p-(--spacing-sm)">
|
|
32677
32936
|
<button
|
|
32678
32937
|
type="button"
|
|
32679
32938
|
variant="none"
|
|
@@ -32710,7 +32969,7 @@ var SettingsDialogComponent = class SettingsDialogComponent {
|
|
|
32710
32969
|
</nav>
|
|
32711
32970
|
</div>
|
|
32712
32971
|
|
|
32713
|
-
<div class="
|
|
32972
|
+
<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
32973
|
@switch (panel()) {
|
|
32715
32974
|
@case ("account") {
|
|
32716
32975
|
<settings-account-panel class="flex min-h-0 flex-1 flex-col" [focusOnOpen]="!navigated()" />
|
|
@@ -32783,6 +33042,12 @@ var SettingsDialogComponent = class SettingsDialogComponent {
|
|
|
32783
33042
|
selector: "x-mark, x-mark-icon, XMark, XMarkIcon",
|
|
32784
33043
|
inputs: ["class"]
|
|
32785
33044
|
},
|
|
33045
|
+
{
|
|
33046
|
+
kind: "directive",
|
|
33047
|
+
type: SidebarInsetDirective,
|
|
33048
|
+
selector: "[sidebarInset]",
|
|
33049
|
+
inputs: ["class"]
|
|
33050
|
+
},
|
|
32786
33051
|
{
|
|
32787
33052
|
kind: "component",
|
|
32788
33053
|
type: SettingsAccountPanelComponent,
|
|
@@ -32825,6 +33090,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
32825
33090
|
PaletteIcon,
|
|
32826
33091
|
KeyboardIcon,
|
|
32827
33092
|
XMarkIcon,
|
|
33093
|
+
SidebarInsetDirective,
|
|
32828
33094
|
SettingsAccountPanelComponent,
|
|
32829
33095
|
SettingsAppearancePanelComponent,
|
|
32830
33096
|
SettingsShortcutsPanelComponent
|
|
@@ -32833,8 +33099,10 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
32833
33099
|
template: `
|
|
32834
33100
|
@if (!duplicate) {
|
|
32835
33101
|
<div dialog #dialog="dialog" [closeLabel]="'settings.close' | transloco" (closed)="call.end($event)">
|
|
32836
|
-
|
|
32837
|
-
|
|
33102
|
+
<!-- The inset layout of the sidebar, in a dialog: the navigation sits on the tinted surface of the
|
|
33103
|
+
dialog itself and the panel is a white card set into it. -->
|
|
33104
|
+
<DialogContent size="xl" class="h-[min(48rem,calc(100dvh-2rem))] flex-row! items-stretch! bg-(--bg-primary-lighter)">
|
|
33105
|
+
<div class="flex w-64 shrink-0 flex-col gap-(--spacing-3xs) p-(--spacing-sm)">
|
|
32838
33106
|
<button
|
|
32839
33107
|
type="button"
|
|
32840
33108
|
variant="none"
|
|
@@ -32871,7 +33139,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
32871
33139
|
</nav>
|
|
32872
33140
|
</div>
|
|
32873
33141
|
|
|
32874
|
-
<div class="
|
|
33142
|
+
<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
33143
|
@switch (panel()) {
|
|
32876
33144
|
@case ("account") {
|
|
32877
33145
|
<settings-account-panel class="flex min-h-0 flex-1 flex-col" [focusOnOpen]="!navigated()" />
|