The main export of this library is the inbox component, it is pre-structured and can be used with a minimum of configuration.
The layout of the inbox can be customized by the consuming application. To get a basic understanding of the inbox component consult the documentation.
For the design to work properly it has to be inserted into an 'talenra-app-frame' component.
The inbox needs to have a specific height given in order to show the list view and the detail view correctly.
In order to see whole inbox, or don't cut off other components, the parent component has to structure the components. A possible style is shown below at the bottom of the README.
The only required input parameter are the attributes, all other inputs are optional
The inboxConfiguration attribute in the inboxState allows to configure the ui of the inbox.
The input/output parameters, available services and content projection are described in the documentation.
Shortcuts for keyboard usage:
| Key | Effect |
|---|---|
| Arrows down/up | previous/next data-item-list |
| Arrows right/left | previous/next data-item-details |
| Enter | execute data item (right-arrow button) |
| Key "y/Y" | open/close data-item-details (side-panel) |
| Key "b/B" | expand data-item-details to maximum size (side-panel open) |
| Key "i/I" | reduce view (information icons are removed) |
| Key "x/X" | open/close attribute view |
| Key "a/A" | open further actions menu |
| Key "f/F" | open/close extended filter view (if filter chosen) |
| Key "Escape" | close the opened menus |
| Key "CMD/Meta" + DataItemClick | select clicked data item |
| Key "Shift" + DataItemClick | select all data items from selected data item to clicked dat item |
The following code snippets are a whole example of a consuming application which is using the inbox component. The example shows how to use the different API possibilities of the inbox component.
Example :// src/app/inbox/inbox.component.ts
import { AfterViewInit, Component, TemplateRef, ViewChild } from '@angular/core';
import {
IAttributeItemInput,
IDataItemId,
IInboxTabBarItem,
IFilterSortSearch,
InboxDetailViewService,
InboxState,
INoteBoxItem,
IPairingAttributes,
IPreset,
IPresetAction,
TPresetOperations,
} from '@talenra/inbox'; // <--
@Component({
selector: 'inbox-root',
templateUrl: './inbox.component.html',
styleUrls: ['./inbox.component.scss'],
})
export class InboxComponent implements AfterViewInit {
/**
* Title which is displayed in the workspace-header
*/
public workspaceTitle = 'Inbox';
/**
* Configuration for the inbox.
*/
public inboxConfiguration = { ... };
/**
* Data that is especially used in the inbox case information tabbar
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public dataItems: any[] = { ... };
/**
* Data that is especially used in the inbox case information tabbar
*/
public presets: IPreset[] = [...];
/**
* lib needs information about the active preset clicked in the side nav
*/
public activePresetName: string[];
/**
* The template for the inbox data items information in the detail view.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@ViewChild('talenraInboxDocuments', { read: TemplateRef }) talenraInboxDocuments!: TemplateRef<any>;
/**
* The template for the inbox data items information in the detail view.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@ViewChild('talenraInboxComments', { read: TemplateRef }) talenraInboxComments!: TemplateRef<any>;
/**
* The template for the inbox data items information in the detail view.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@ViewChild('talenraInboxHistory', { read: TemplateRef }) talenraInboxHistory!: TemplateRef<any>;
/**
* The detail view tabs for the inbox data items information.
*/
detailViewTabs: IInboxTabBarItem[] = [];
/**
* The note box items for the detail view.
*/
INoteBoxItems: INoteBoxItem[] | undefined = undefined;
/**
* The headerHidden is a boolean to hide the header.
*/
headerHidden = false;
/**
* The noScroll is a boolean to save the state if no scroll is available.
*/
noScroll = false;
/**
* The headerEdgeCase is a boolean to save the state if the header is in the edge case.
*/
headerEdgeCase = false;
/**
* The isLoading state, activates the skeleton loading for the whole inbox.
*/
isLoading = false;
/**
* The isRefreshing state, activates the skeleton loading for the data item list and detail view.
*/
isRefreshing = false;
/**
* The selectDataItem is a data item for the select component.
*/
selectDataItem = {
identifier: 'workItemTemplateID',
value: '0',
};
/**
* Empty array with the single purpose to be used in the template for looping
* (produce long content for debugging).
*/
items: string[] = new Array(20).fill('');
/**
* The required attributes input for the inbox component.
* REQUIRED for inbox component.
*/
attributes: IAttributeItemInput[] = [...];
/**
* The pairing attributes input for the inbox component.
*/
pairingAttributes: IPairingAttributes[] = [...];
/**
* The locked data items for the inbox component.
*/
lockedDataItems: IDataItemId[] = [...];
/**
* Checkbox for the example components in the detail view files
*/
exampleCheckboxForm: FormGroup = new FormGroup({
check_1: new FormControl(false),
});
/**
* The pre link of the routerLink.
*/
routerLink = '/inbox/';
/**
* Prefix for path displayed in the workspace-header
*/
private readonly workspacePathPrefix: BreadcrumbFragment[] = [{ label: 'Talenra Guide' }, { label: 'Inbox' }];
/**
* Path which is displayed in the workspace-header
*/
workspacePath: BreadcrumbFragment[] = [...this.workspacePathPrefix];
/**
* The constructor of the inbox demo component.
*/
constructor(
public inboxDemoService: InboxDemoService,
private router: Router,
public inboxDetailViewService: InboxDetailViewService,
public inboxState: InboxState
) {
// set the initial active preset name
this.activePresetName = ['Meine Inbox'];
// set the configuration
this.inboxState.setInboxConfiguration(this.inboxConfiguration);
}
/**
* The ngAfterViewInit lifecycle hook.
*/
ngAfterViewInit(): void {
// get the template in the next lifecycle hook, because the viewChild is not available yet
Promise.resolve().then(() => {
// set the presets
this.inboxDemoService.presets$.subscribe((presets: IPreset[]) => {
if (presets.length > 0) {
this.presets = presets;
}
});
// set the presets in service to be available for the app component and side nav
this.inboxDemoService.setPresets(this.presets);
// let the data be available for app component to create view windows in the side nav
this.inboxDemoService.recursiveActivePreset$.subscribe((activeViewName: string[]) => {
if (activeViewName.length > 0) {
// set workspace title accordingly
const viewGroupChild = activeViewName[1] ? activeViewName[1] : undefined;
this.workspaceTitle = activeViewName[0];
this.workspacePath = [...this.workspacePathPrefix, { label: activeViewName[0] }];
if (viewGroupChild !== undefined) {
this.workspaceTitle += ': ' + viewGroupChild;
this.workspacePath.push({ label: viewGroupChild });
}
// set active view name
this.activePresetName = activeViewName;
}
});
// Restore preset/view based on current URL
const preset: string[] | null = this.inboxDemoService.presetFromUri(this.router.url);
preset && this.inboxDemoService.setRecursiveActivePreset(preset);
// create the detail view tabs
this.detailViewTabs = [
{
label: 'documents',
icon: 'description',
counter: new BehaviorSubject<number>(8),
useScroll: false,
detailViewTabContentTemplate: this.talenraInboxDocuments,
},
{
label: 'comments',
icon: 'chat-bubble-outline',
counter: new BehaviorSubject<number>(100),
useScroll: true,
detailViewTabContentTemplate: this.talenraInboxComments,
},
{
label: 'history',
icon: 'history',
useScroll: false,
detailViewTabContentTemplate: this.talenraInboxHistory,
},
];
});
}
/**
* The onCaseInteractions function is called when the case interactions are clicked.
* @param event
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onCaseInteractions(event: any): void {
console.log('onCaseInteractions', event);
}
/**
* The onSelectedMainAttributes function is called when the main attributes are selected.
* @param event The selected main attributes.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onSelectedMainAttributes(event: any) {
console.log('onSelectedMainAttributes', event);
}
/**
* The onSelectedDetailAttributes function is called when the detail attributes are selected.
* @param event The selected detail attributes.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onSelectedDetailAttributes(event: any) {
console.log('onSelectedDetailAttributes', event);
}
/**
* The onSelectedAllAttributes function is called when all attributes are selected.
* @param event All the selected attributes.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onSelectedAllAttributes(event: any) {
console.log('onSelectedAllAttributes', event);
}
/**
* The clicked data item function is called when a data item is clicked.
* @param event
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unused-vars
onClickedDataItem(event: any): void {
console.log('onClickedCase', event);
}
/**
* The virtual scroller position is emitted, or a descriptive number to reflect a state
* -2: the inbox has a buggy loop, the header should be fixed and not be trying to hide for the remaining usage
* -1: the inbox is not scrollable (to few items)
* 0: the inbox is at the top
* > 0: the inbox is not at the top
*
* Note: the html implementation and order is important (see html)
* @param event The virtual scroller position.
*/
onVirtualScrollerPosition(event: number): void {
switch (event) {
// value if the header bugs, because of the collapsable header
case -2:
this.headerEdgeCase = true;
break;
default:
this.noScroll = event === -1;
this.headerHidden = event > 0;
break;
}
}
/**
* The onPresetAction function is called when a preset action is triggered.
* @param presetAction The preset action.
*/
onPresetAction(presetAction: IPresetAction): void {
// set the presets
this.inboxDemoService.setPresets(presetAction.presets);
// set the preset action
this.inboxDemoService.setPresetAction(presetAction);
// please handle specific preset actions here
// Option A: if the ACTIVE preset is deleted or hidden, the app have to switch to another preset or group
if (
(this.inboxDemoService.presetAction$.value.operation === TPresetOperations.Delete &&
JSON.stringify(this.inboxDemoService.recursiveActivePreset$.value) ===
JSON.stringify([
this.inboxDemoService.presetAction$.value.groupName,
this.inboxDemoService.presetAction$.value.presetName,
])) ||
(this.inboxDemoService.presetAction$.value.operation === TPresetOperations.VisibilityOff &&
JSON.stringify(this.inboxDemoService.recursiveActivePreset$.value) ===
JSON.stringify([
this.inboxDemoService.presetAction$.value.groupName,
this.inboxDemoService.presetAction$.value.presetName,
]))
) {
this.inboxDemoService.setRecursiveActivePreset([this.inboxDemoService.presetAction$.value.groupName]);
this.router.navigate([this.routerLink + this.inboxDemoService.presetAction$.value.groupName.toLowerCase()]);
}
// Option A: if a new preset is created or a preset is updated, the app should switch to the new or updated preset
if (
this.inboxDemoService.presetAction$.value.operation === PresetOperations.Create ||
this.inboxDemoService.presetAction$.value.operation === PresetOperations.Update
) {
if (!this.inboxDemoService.presetAction$.value.presetName) return;
this.inboxDemoService.setRecursiveActivePreset([
this.inboxDemoService.presetAction$.value.groupName,
this.inboxDemoService.presetAction$.value.presetName,
]);
this.router.navigate([
this.routerLink +
this.inboxDemoService.presetAction$.value.groupName.toLowerCase() +
'~' +
this.inboxDemoService.presetAction$.value.presetName?.toLowerCase(),
]);
}
if (
this.inboxDemoService.presetAction$.value.operation === TPresetOperations.UpdateRename ||
(this.inboxDemoService.presetAction$.value.operation === TPresetOperations.Rename &&
JSON.stringify(this.inboxDemoService.recursiveActivePreset$.value) ===
JSON.stringify([
this.inboxDemoService.presetAction$.value.groupName,
this.inboxDemoService.presetAction$.value.presetName,
]))
) {
if (!this.inboxDemoService.presetAction$.value.newPresetName) return;
this.inboxDemoService.setRecursiveActivePreset([
this.inboxDemoService.presetAction$.value.groupName,
this.inboxDemoService.presetAction$.value.newPresetName,
]);
this.router.navigate([
this.routerLink +
this.inboxDemoService.presetAction$.value.groupName.toLowerCase() +
'~' +
this.inboxDemoService.presetAction$.value.newPresetName?.toLowerCase(),
]);
}
console.log('onPresetAction', presetAction);
}
/**
* The onFilterSortSearchEmitter function is called when the filter sort search is triggered.
* @param event
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
onFilterSortSearchEmitter(event: IFilterSortSearch) {
console.log('onFilterSortSearchEmitter', event);
}
/**
* The onRefreshingChange function is called when the refreshing state changes.
* @param $event The refreshing state.
*/
onRefreshingChange($event: boolean) {
console.log('onRefreshingChange', $event);
// set the refreshing state
this.isRefreshing = $event;
// simulate a refresh
setTimeout(() => {
// change the data items to simulate an api call
if (this.dataItems.length === 12) {
this.dataItems = this.sampleData.sampleData;
} else {
this.dataItems = this.sampleData.sampleDataShort;
}
this.isRefreshing = false;
}, 1000);
}
/**
* The onPaginatorStateChange function is called when the paginator state changes.
* @param paginatorState The current paginator state.
*/
onPaginatorStateChange(paginatorState: PaginatorState) {
console.log('onPaginatorStateChange', paginatorState);
this.paginatorState = paginatorState;
this.isRefreshing = true;
setTimeout(() => {
this.dataItems = this.sampleData.sampleData.slice(
(this.paginatorState.currentPage - 1) * this.paginatorState.itemsPerPage,
this.paginatorState.currentPage * this.paginatorState.itemsPerPage
);
this.isRefreshing = false;
}, 500);
}
}In the app component new functions are included to create views as sideNavItems. Furthermore, the active view name should be defined.
Example :// src/app/app.component.ts
import { Component, OnDestroy, OnInit } from '@angular/core';
import { LayoutService } from '@talenra/components/layout-service';
import { SidenavItem } from '@talenra/components/app-layout';
import { Subject, takeUntil } from 'rxjs';
import { AppService } from './app.service';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
})
export class AppComponent implements OnInit, OnDestroy {
title = 'talenra-advanced-inbox-app';
/**
* The sidenav items to be displayed in the app component.
*/
public sidenavItems: SidenavItem[] = sidenavItems;
/**
* Storage for subscriptions of inbox interactions
*/
private subscriptions: Subscription = new Subscription();
// Sidenav items to be prepended _before_ the groups/views
private sidenavItemsAppend: SidenavItem[] = [
{
identifier: 'bb1bf149-59e2-4800-a9d9-699d90c13563',
label: 'Inbox Demo Simple',
icon: 'build',
destination: '/inbox/demo-simple',
children: [],
isExpanded: true,
},
];
constructor(
private router: Router,
private inboxDemoService: InboxDemoService,
private sidenavService: SidenavService
) {}
ngOnInit(): void {
// HeaderDrawer: Example on how to trigger delayed content update
setTimeout(() => {
// Trigger content update
this.drawerHasExtraItems = true;
// Notify the HeaderDrawer that the content has changed
this.drawerTrackingRef = `drawer-${nextId++}`;
}, 1500);
// State: Store the sidenav's expanded state in local storage
this.subscriptions.add(
this.sidenavService.isExpanded$.subscribe((value: boolean) => {
localStorage.setItem('sidenavExpanded', value.toString());
})
);
// Inbox: Subscribe to preset options updates
this.subscriptions.add(
this.inboxDemoService.presets$.subscribe((options: IPreset[]) => {
this.updateSidenav(options);
})
);
// Inbox: Subscribe to navigation events
this.subscriptions.add(
this.router.events.pipe(filter((event: Event) => event instanceof NavigationEnd)).subscribe((event) => {
this.updateActiveView((event as NavigationEnd).urlAfterRedirects);
})
);
// Get deployed app versions (based on deployment folders of server)
this.deployedAppVersions$ = this.getDeployedAppVersions();
}
ngOnDestroy() {
this.subscriptions.unsubscribe();
}
/**
* Returns the signature of the active view from a given URI.
* Furthermore, it updates the active view in the app service.
*/
updateActiveView(uri: string): void {
const preset: string[] | null = this.inboxDemoService.presetFromUri(uri);
preset && this.inboxDemoService.setRecursiveActivePreset(preset);
}
/**
* Updates sidenav items based on preset options (grouped views) passed from inbox component
*/
private updateSidenav(groups: IPreset[]): void {
// Helper: Creates the destination string
const buildDestination = (frags: string[]): string =>
'/inbox/' + frags.join(InboxDemoService.URL_SEPARATOR).toLowerCase();
// Temporary storage for the grouped views
const viewGroups: SidenavItem[] = [];
// Create the group item
groups.forEach((group: IPreset) => {
// Create the group's view items (children of the group), ignores not-visible views
const views: SidenavItem[] = [];
group.children?.forEach((view: IPreset) => {
(view.visible || view.visible === undefined) &&
views.push({
label: view.name,
icon: 'circle',
destination: buildDestination([group.name, view.name]),
});
});
// Create the group item and add the child views
const item: SidenavItem = {
identifier: group.identifier,
label: group.name,
icon: group.name === 'Warteliste' ? 'update' : 'team-menu',
destination: buildDestination([group.name]),
children: views,
};
const currentItem: SidenavItem[] = this.sidenavService.getItemsByIdentifier(group.identifier);
const isExpanded: boolean = currentItem[0]?.isExpanded ?? true;
viewGroups.push({ ...item, isExpanded });
});
// Find the "inbox" sidenav item
const inboxSidenavItem: SidenavItem = this.sidenavService.getItemsByIdentifier(inboxIdentifier)[0];
if (!inboxSidenavItem) return;
// Find the index of the "inbox" sidenav item. Return if not found or not a top-level item.
const inboxSidenavItemIndex = this.sidenavService.getIndicesByIdentifier(inboxIdentifier)[0];
if (inboxSidenavItemIndex.length !== 1) return;
// Update the "inbox" sidenav item's children and update the "inbox" item in the sidenav.
inboxSidenavItem.children = [...viewGroups, ...this.sidenavItemsAppend];
this.sidenavItems[inboxSidenavItemIndex[0]] = inboxSidenavItem;
this.sidenavItems = [...this.sidenavItems];
}
}Shows how to handle click events on the sideNavItems
Example :<!-- src/app/app.component.html -->
<talenra-app-frame
issuerName="Talenra"
appName="Talenra Guide"
[appEnv]="env"
[appVersion]="version"
[appLang]="lang"
[(sidenavItems)]="sidenavItems"
<router-outlet />
</talenra-app-frame>
<talenra-global-styles></talenra-global-styles>Helper Service Class for simplified communication between app.component and src/app/inbox/inbox.component in the consuming application.
Example ://src/app/app.service.ts
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
/**
* Services to communicate between the inbox and app component.
* ActiveViewName & preSetOptions are necessary.
*/
@Injectable({
providedIn: 'root',
})
export class InboxDemoService {
/** Character used to separate group name and view name in URI */
public static readonly URL_SEPARATOR = '~';
/** Base path of the inbox */
public static readonly BASE_PATH = 'inbox';
/** Path of the inbox simple app */
public static readonly PATH_SIMPLE_APP = 'simple-app';
public readonly presets$ = new BehaviorSubject<IPreset[]>([]);
public readonly recursiveActivePreset$ = new BehaviorSubject<string[]>([]);
public readonly presetAction$ = new BehaviorSubject<IPresetAction>({
presets: [],
operation: TPresetOperations.None,
groupName: '',
presetName: '',
newPresetName: '',
});
public setPresets(key: IPreset[]): void {
this.presets$.next(key);
}
public setRecursiveActivePreset(key: string[]): void {
this.recursiveActivePreset$.next(key);
}
public setPresetAction(key: IPresetAction): void {
this.presetAction$.next(key);
}
/**
* Parses a given URI and returns the preset options that match the URI.
*
* Examples:
* - /inbox/warteliste~view%20b -> ['Warteliste', 'View B']
* - /inbox/gesamtliste -> ['Gesamtliste']
* - /inbox/invalid -> ['default']
* - /inbox/simple-demo -> ['Simple Demo']
* - /invalid-base-path -> null
*/
public presetFromUri(uri: string): string[] | null {
// Helper to sync/match the URI to the preset options recursively
const matchNames = (haystack: IPreset[], needles: string[], matches: string[] = []): string[] => {
const match: IPreset | undefined = haystack.find((item: IPreset) => item.name.toLowerCase() === needles[0]);
match && matches.push(match.name);
return match?.children ? matchNames(match.children, needles.slice(1), matches) : matches;
};
// Split the URI into fragments and decode them
const frags: string[] = decodeURI(uri.substring(1)).split(InboxDemoService.URL_SEPARATOR);
// If the URI does not start with the base path, it is not a preset
if (!frags[0].startsWith(InboxDemoService.BASE_PATH)) return null;
// If the URI is the simple app, return it
if (frags[0] === InboxDemoService.BASE_PATH + '/' + InboxDemoService.PATH_SIMPLE_APP) return [frags[0]];
// Remove the base path from the first fragment
frags[0] = frags[0].substring(InboxDemoService.BASE_PATH.length + 1);
// Get matches
const matches: string[] = matchNames(this.presets$.value, frags);
// Return matches or default
return matches.length > 0 ? matches : ['Inbox Demo'];
}
}<!-- src/app/inbox/inbox.component.html -->
<talenra-workspace-simple>
<div class="inbox-app">
<talenra-workspace-header
[path]="workspacePath"
[title]="workspaceTitle"
class="page-header"
[class.header-hidden]="!headerEdgeCase && headerHidden && !noScroll" />
<talenra-inbox
(clickedDataItemEmitter)="onClickedCase($event)"
(dataItemsInteractions)="onCaseInteractions($event)"
(filterSortSearchEmitter)="onFilterSortSearchEmitter($event)"
(presetAction)="onPresetAction($event)"
(refreshingChange)="onRefreshingChange($event)"
(selectedAllAttributesEmitter)="onSelectedAllAttributes($event)"
(selectedDetailAttributesEmitter)="onSelectedDetailAttributes($event)"
(selectedMainAttributesEmitter)="onSelectedMainAttributes($event)"
(virtualScrollerPosition)="onVirtualScrollerPosition($event)"
(paginatorStateChange)="onPaginatorStateChange($event)"
[activePresetName]="activeViewName"
[attributes]="attributesExpanded"
[dataItems]="dataItems"
[paginatorState]="paginatorState"
[detailViewTabs]="detailViewTabs"
[loading]="isLoading"
[lockedDataItems]="lockedDataItems"
[INoteBoxItems]="INoteBoxItems"
[pairingAttributes]="pairingAttributes"
[presets]="preSetOptions"
[refreshing]="isRefreshing"
[selectDataItem]="selectDataItem"
usePagination
class="talenra-inbox">
<div custom-toolbar-space>Custom Toolbar Space</div>
<div further-actions-toolbar-menu>Further Actions Toolbar</div>
<div further-actions-data-item-menu>Further Actions Data Item</div>
<div custom-data-items-list>
<talenra-scroll-container *ngIf="!isRefreshing" class="custom-data-items-list"> ... </talenra-scroll-container>
<div *ngIf="isRefreshing">Loading...</div>
</div>
</talenra-inbox>
</div>
</talenra-workspace-simple>
<!-- detail-view tabs -->
<ng-template #talenraInboxDocuments>...</ng-template>
<ng-template #talenraInboxComments>...</ng-template>
<ng-template #talenraInboxHistory>
<talenra-scroll-container class="custom-scroll">
<h1 class="panel-title">Historie</h1>
<div *ngFor="let item of items" class="inline-documents">
<p class="pair">
<span class="documents">Wurde erfasst und ist in System eingepflegt</span>
<span class="documents-entry">16.09.2022</span>
</p>
</div>
</talenra-scroll-container>
</ng-template>// src/app/app.component.scss
@use '@talenra/stylebox';
.inbox-app {
display: flex;
flex-wrap: nowrap;
flex-direction: column;
height: 100%;
width: 100%;
}
.page-header {
opacity: 1;
height: 92px;
transition:
height 0.3s,
opacity 0.3s;
&.header-hidden {
opacity: 0;
height: 0;
}
}
.talenra-inbox {
position: relative;
flex-grow: 1;
}
.panel-title {
@include stylebox.type-use('s');
margin-top: stylebox.px-to-rem(21);
margin-bottom: stylebox.px-to-rem(13);
padding-bottom: 5px;
}
.custom-scroll {
position: absolute;
inset: 0;
}
.custom-data-items-list {
position: absolute;
inset: 0;
margin-right: stylebox.px-to-rem(16);
}
.inline-documents {
margin-left: auto;
margin-right: 0;
display: flex;
}
.pair {
margin: 0 0 stylebox.px-to-rem(13);
.documents {
display: block;
@include stylebox.type-use('s');
}
.documents-entry {
display: block;
@include stylebox.type-use('s');
color: stylebox.color-get('black', 60);
}
}
[custom-toolbar-space] {
display: flex;
gap: stylebox.px-to-rem(4);
}