diff --git a/frontend/src/app/app.component.ts b/frontend/src/app/app.component.ts index 2017dd898a..dba60dda81 100644 --- a/frontend/src/app/app.component.ts +++ b/frontend/src/app/app.component.ts @@ -57,9 +57,20 @@ export class AppComponent implements OnInit { document.documentElement.style.setProperty('--vh', `${vh}px`); } - resizeMenu(type: 'COLLAPSE' | 'EXPAND' | 'NO_MARGIN') { + resizeMenu(type: 'COLLAPSE' | 'EXPAND' | 'NO_MARGIN' | 'HORIZONTAL') { const progressFooter = document.getElementById('block-progress-footer'); switch (type) { + case 'HORIZONTAL': { + // Rail width is zeroed by the `layout-horizontal` root class, so the page + // only needs its left gutter removed; the top offset comes from --header-height. + document.body.style.setProperty('--header-width', '0px'); + document.getElementById('main-content')!.style.left = '0'; + document.getElementById('main-content')!.removeAttribute('main-collapse-menu'); + if (progressFooter) { + progressFooter.style.paddingLeft = '48px'; + } + break; + } case 'COLLAPSE': { document.body.style.setProperty('--header-width', 'var(--header-width-collapse)'); document.getElementById('main-content')!.style.left = 'var(--header-width-collapse)'; diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 648edeb696..f9af9bf560 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -149,6 +149,7 @@ import { PolicyRepositoryService } from './services/policy-repository.service'; import { RelayerAccountsService } from './services/relayer-accounts.service'; import { RelayerAccountsComponent } from './views/relayer-accounts/relayer-accounts.component'; import { TreeTableModule } from 'primeng/treetable'; +import { MenubarModule } from 'primeng/menubar'; import { CredentialsPanelComponent } from './components/credentials/credentials-panel/credentials-panel.component'; const GuardianPreset = definePreset(Aura, { @@ -290,7 +291,8 @@ const GuardianPreset = definePreset(Aura, { CardModule, ToggleSwitchModule, AngularSvgIconModule.forRoot(), - TreeTableModule + TreeTableModule, + MenubarModule ], providers: [ WebSocketService, diff --git a/frontend/src/app/components/notification/notification.component.html b/frontend/src/app/components/notification/notification.component.html index ce1c2affcd..dfec6e7bd0 100644 --- a/frontend/src/app/components/notification/notification.component.html +++ b/frontend/src/app/components/notification/notification.component.html @@ -2,9 +2,10 @@
- + {{ unreadNotifications }}
diff --git a/frontend/src/app/components/notification/notification.component.scss b/frontend/src/app/components/notification/notification.component.scss index 1a8871dec3..2d00985c82 100644 --- a/frontend/src/app/components/notification/notification.component.scss +++ b/frontend/src/app/components/notification/notification.component.scss @@ -57,6 +57,18 @@ } } +.badge-dot { + width: 8px; + height: 8px; + border-radius: 50%; + background-color: #ff432a; + cursor: pointer; + + &:hover { + filter: brightness(1.2); + } +} + .notification-position { pointer-events: none; position: fixed; diff --git a/frontend/src/app/components/notification/notification.component.ts b/frontend/src/app/components/notification/notification.component.ts index 88da8880a6..2407c2c4a4 100644 --- a/frontend/src/app/components/notification/notification.component.ts +++ b/frontend/src/app/components/notification/notification.component.ts @@ -1,4 +1,4 @@ -import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core'; +import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'; import { Router } from '@angular/router'; import { NotificationType, NotifyAPI, } from '@guardian/interfaces'; import { ToastrService } from 'ngx-toastr'; @@ -19,6 +19,9 @@ export class NotificationComponent implements OnInit { menuOpened: boolean = false; subscription = new Subscription(); + /** Show a plain red dot instead of the unread count (used in the collapsed menu). */ + @Input() compact: boolean = false; + @Output() menuOpenedChange = new EventEmitter(); viewDetails($event: MouseEvent, notification: any) { diff --git a/frontend/src/app/modules/policy-engine/policies/policies.component.scss b/frontend/src/app/modules/policy-engine/policies/policies.component.scss index 2d31db4b6b..1c6f9a23e4 100644 --- a/frontend/src/app/modules/policy-engine/policies/policies.component.scss +++ b/frontend/src/app/modules/policy-engine/policies/policies.component.scss @@ -1154,7 +1154,7 @@ .options-footer { padding: 12px 48px; - padding-left: calc(var(--header-width-expand) + 48px); + padding-left: calc(var(--header-width, var(--header-width-expand)) + 48px); display: flex; justify-content: space-between; position: fixed; diff --git a/frontend/src/app/modules/policy-engine/policy-viewer/blocks/global-events-reader-block/global-events-reader-block.component.scss b/frontend/src/app/modules/policy-engine/policy-viewer/blocks/global-events-reader-block/global-events-reader-block.component.scss index 1769f57f08..7e30b3c5ef 100644 --- a/frontend/src/app/modules/policy-engine/policy-viewer/blocks/global-events-reader-block/global-events-reader-block.component.scss +++ b/frontend/src/app/modules/policy-engine/policy-viewer/blocks/global-events-reader-block/global-events-reader-block.component.scss @@ -316,7 +316,7 @@ width: 100%; border-top: 1px solid var(--color-grey-3, #E1E7EF); background: var(--guardian-background, #FFF); - padding-left: calc(var(--header-width-expand) + 48px); + padding-left: calc(var(--header-width, var(--header-width-expand)) + 48px); transition: padding-left 0.125s ease-in-out; z-index: 999; } diff --git a/frontend/src/app/modules/policy-engine/policy-viewer/blocks/global-events-writer-block/global-events-writer-block.component.scss b/frontend/src/app/modules/policy-engine/policy-viewer/blocks/global-events-writer-block/global-events-writer-block.component.scss index 058b6f8ef3..d0530e9548 100644 --- a/frontend/src/app/modules/policy-engine/policy-viewer/blocks/global-events-writer-block/global-events-writer-block.component.scss +++ b/frontend/src/app/modules/policy-engine/policy-viewer/blocks/global-events-writer-block/global-events-writer-block.component.scss @@ -241,7 +241,7 @@ width: 100%; border-top: 1px solid var(--color-grey-3, #E1E7EF); background: var(--guardian-background, #FFF); - padding-left: calc(var(--header-width-expand) + 48px); + padding-left: calc(var(--header-width, var(--header-width-expand)) + 48px); transition: padding-left 0.125s ease-in-out; z-index: 999; } diff --git a/frontend/src/app/modules/policy-engine/policy-viewer/blocks/request-document-block/request-document-block.component.scss b/frontend/src/app/modules/policy-engine/policy-viewer/blocks/request-document-block/request-document-block.component.scss index 4a3f1259b0..98c1a4c47b 100644 --- a/frontend/src/app/modules/policy-engine/policy-viewer/blocks/request-document-block/request-document-block.component.scss +++ b/frontend/src/app/modules/policy-engine/policy-viewer/blocks/request-document-block/request-document-block.component.scss @@ -179,7 +179,7 @@ width: 100%; border-top: 1px solid var(--color-grey-3, #e1e7ef); background: var(--guardian-background, #fff); - padding-left: calc(var(--header-width-expand) + 48px); + padding-left: calc(var(--header-width, var(--header-width-expand)) + 48px); transition: padding-left 0.125s ease-in-out; margin-top: 20px; @@ -548,7 +548,7 @@ width: 100%; border-top: 1px solid var(--color-grey-3, #e1e7ef); background: var(--guardian-background, #fff); - padding-left: calc(var(--header-width-expand) + 48px); + padding-left: calc(var(--header-width, var(--header-width-expand)) + 48px); transition: padding-left 0.125s ease-in-out; margin-top: 20px; left: 50%; diff --git a/frontend/src/app/modules/policy-engine/policy-viewer/blocks/roles-block/roles-block.component.scss b/frontend/src/app/modules/policy-engine/policy-viewer/blocks/roles-block/roles-block.component.scss index acf269bbda..073dd6f946 100644 --- a/frontend/src/app/modules/policy-engine/policy-viewer/blocks/roles-block/roles-block.component.scss +++ b/frontend/src/app/modules/policy-engine/policy-viewer/blocks/roles-block/roles-block.component.scss @@ -131,7 +131,7 @@ form { width: 100%; border-top: 1px solid var(--color-grey-3, #E1E7EF); background: var(--guardian-background, #FFF); - padding-left: calc(var(--header-width-expand) + 48px); + padding-left: calc(var(--header-width, var(--header-width-expand)) + 48px); transition: padding-left 0.125s ease-in-out; z-index: 999; } diff --git a/frontend/src/app/modules/policy-engine/policy-viewer/policy-viewer/policy-viewer.component.scss b/frontend/src/app/modules/policy-engine/policy-viewer/policy-viewer/policy-viewer.component.scss index 464e4b1632..db376de266 100644 --- a/frontend/src/app/modules/policy-engine/policy-viewer/policy-viewer/policy-viewer.component.scss +++ b/frontend/src/app/modules/policy-engine/policy-viewer/policy-viewer/policy-viewer.component.scss @@ -878,7 +878,7 @@ a.go-back-link svg { .progress-footer { padding: 12px 48px; - padding-left: calc(var(--header-width-expand) + 48px); + padding-left: calc(var(--header-width, var(--header-width-expand)) + 48px); display: flex; justify-content: space-between; position: fixed; diff --git a/frontend/src/app/modules/policy-engine/policy-viewer/progress-tracker/progress-tracker.component.scss b/frontend/src/app/modules/policy-engine/policy-viewer/progress-tracker/progress-tracker.component.scss index 1064e3f2ca..3277e3a46e 100644 --- a/frontend/src/app/modules/policy-engine/policy-viewer/progress-tracker/progress-tracker.component.scss +++ b/frontend/src/app/modules/policy-engine/policy-viewer/progress-tracker/progress-tracker.component.scss @@ -15,7 +15,7 @@ .progress-footer { padding: 12px 48px; - padding-left: calc(var(--header-width-expand) + 48px); + padding-left: calc(var(--header-width, var(--header-width-expand)) + 48px); display: flex; justify-content: space-between; position: fixed; diff --git a/frontend/src/app/modules/policy-engine/project-data-export/project-data-export.component.scss b/frontend/src/app/modules/policy-engine/project-data-export/project-data-export.component.scss index 1ba74b3af8..e0a40aacfe 100644 --- a/frontend/src/app/modules/policy-engine/project-data-export/project-data-export.component.scss +++ b/frontend/src/app/modules/policy-engine/project-data-export/project-data-export.component.scss @@ -462,7 +462,7 @@ a { .progress-footer { padding: 12px 48px; - padding-left: calc(var(--header-width-expand) + 48px); + padding-left: calc(var(--header-width, var(--header-width-expand)) + 48px); display: flex; justify-content: space-between; position: fixed; diff --git a/frontend/src/app/services/menu-layout.service.ts b/frontend/src/app/services/menu-layout.service.ts new file mode 100644 index 0000000000..ee6e31f65c --- /dev/null +++ b/frontend/src/app/services/menu-layout.service.ts @@ -0,0 +1,72 @@ +import { Injectable } from '@angular/core'; +import { BehaviorSubject, Observable } from 'rxjs'; + +export type MenuLayout = 'vertical' | 'horizontal'; + +export interface MenuLayoutOption { + label: string; + value: MenuLayout; + icon: string; +} + +const MENU_LAYOUT_STORAGE_KEY = 'MAIN_HEADER_LAYOUT'; +const HORIZONTAL_CLASS = 'layout-horizontal'; + +@Injectable({ + providedIn: 'root' +}) +export class MenuLayoutService { + public readonly layouts: MenuLayoutOption[] = [ + { label: 'Vertical', value: 'vertical', icon: 'pi-bars' }, + { label: 'Horizontal', value: 'horizontal', icon: 'pi-window-maximize' } + ]; + + private readonly layout$ = new BehaviorSubject(this.readStoredLayout()); + + constructor() { + this.applyLayoutClass(this.layout$.value); + } + + public get layout(): MenuLayout { + return this.layout$.value; + } + + public get changes(): Observable { + return this.layout$.asObservable(); + } + + public setLayout(layout: MenuLayout): void { + const resolved = this.findLayout(layout).value; + if (resolved === this.layout$.value) { + return; + } + try { + localStorage.setItem(MENU_LAYOUT_STORAGE_KEY, resolved); + } catch (error) { + console.error(error); + } + this.applyLayoutClass(resolved); + this.layout$.next(resolved); + } + + public toggle(): void { + this.setLayout(this.layout$.value === 'vertical' ? 'horizontal' : 'vertical'); + } + + private readStoredLayout(): MenuLayout { + try { + return this.findLayout(localStorage.getItem(MENU_LAYOUT_STORAGE_KEY)).value; + } catch (error) { + console.error(error); + return this.layouts[0].value; + } + } + + private applyLayoutClass(layout: MenuLayout): void { + document.documentElement.classList.toggle(HORIZONTAL_CLASS, layout === 'horizontal'); + } + + private findLayout(layout: string | null): MenuLayoutOption { + return this.layouts.find((item) => item.value === layout) || this.layouts[0]; + } +} diff --git a/frontend/src/app/utils/balance.ts b/frontend/src/app/utils/balance.ts new file mode 100644 index 0000000000..3be48a6f06 --- /dev/null +++ b/frontend/src/app/utils/balance.ts @@ -0,0 +1,13 @@ +const HBAR_SYMBOL = 'ℏ'; + +/** Standardise a balance for display: 3 decimals + ℏ. Non-numeric input is passed through. */ +export function formatBalance(balance: string | number | null | undefined): string { + if (balance === null || balance === undefined || balance === '') { + return ''; + } + const value = typeof balance === 'number' ? balance : parseFloat(balance); + if (!isFinite(value)) { + return typeof balance === 'string' ? balance : ''; + } + return `${value.toFixed(3)} ${HBAR_SYMBOL}`; +} diff --git a/frontend/src/app/utils/index.ts b/frontend/src/app/utils/index.ts index 66812ac968..1c64601029 100644 --- a/frontend/src/app/utils/index.ts +++ b/frontend/src/app/utils/index.ts @@ -3,3 +3,5 @@ export { CategoryAccess, CategoryDetails, CategoryGroup } from "./permissions-ca export { EntityAccess, EntityGroup } from "./permissions-entity"; export { PermissionsGroup } from "./permissions"; export { MergeUtils } from "./merge-utils"; +export { getUserInitials } from "./user-initials"; +export { formatBalance } from "./balance"; diff --git a/frontend/src/app/utils/user-initials.ts b/frontend/src/app/utils/user-initials.ts new file mode 100644 index 0000000000..f32f283677 --- /dev/null +++ b/frontend/src/app/utils/user-initials.ts @@ -0,0 +1,14 @@ +/** + * Two-letter avatar initials: prefer the first two capital letters, otherwise the + * first two characters uppercased. Shared by the header avatar and the profile page. + */ +export function getUserInitials(username: string | null | undefined): string { + if (!username) { + return '?'; + } + const caps = username.match(/[A-Z]/g); + if (caps && caps.length >= 2) { + return caps.slice(0, 2).join(''); + } + return username.slice(0, 2).toUpperCase(); +} diff --git a/frontend/src/app/views/admin/settings-view/settings-view.component.scss b/frontend/src/app/views/admin/settings-view/settings-view.component.scss index d44d529d0d..7e855cbac8 100644 --- a/frontend/src/app/views/admin/settings-view/settings-view.component.scss +++ b/frontend/src/app/views/admin/settings-view/settings-view.component.scss @@ -42,13 +42,14 @@ .actions-container { position: fixed; - left: 270px; + left: var(--header-width, var(--header-width-expand)); right: 0; bottom: 0; height: 64px; display: flex; align-items: center; justify-content: flex-end; + transition: left 0.125s ease-in-out; background-color: var(--color-grey-white); border-top: 1px solid var(--color-grey-3, #E1E7EF); padding: 0 48px; diff --git a/frontend/src/app/views/branding/branding.component.scss b/frontend/src/app/views/branding/branding.component.scss index 5892a6fea4..ae9b4cdbc9 100644 --- a/frontend/src/app/views/branding/branding.component.scss +++ b/frontend/src/app/views/branding/branding.component.scss @@ -1,3 +1,13 @@ +.guardian-page { + // The shared .guardian-page is height: 100% + flex, so a plain padding-bottom + // can't reserve room for the fixed actions bar — overflowing content scrolls + // flush under it. Let the page grow with its content instead so the padding + // becomes real scroll space below the last card. + height: auto; + min-height: 100%; + padding-bottom: 88px; +} + .not-exist { position: absolute; left: 48px; @@ -55,13 +65,14 @@ .actions-container { position: fixed; - left: 270px; + left: var(--header-width, var(--header-width-expand)); right: 0; bottom: 0; height: 64px; display: flex; align-items: center; justify-content: space-between; + transition: left 0.125s ease-in-out; background-color: var(--color-grey-white); border-top: 1px solid var(--color-grey-3, #E1E7EF); padding: 0 48px; diff --git a/frontend/src/app/views/new-header/menu.model.ts b/frontend/src/app/views/new-header/menu.model.ts index 556602aa15..dd180a770a 100644 --- a/frontend/src/app/views/new-header/menu.model.ts +++ b/frontend/src/app/views/new-header/menu.model.ts @@ -3,8 +3,10 @@ import { UserPermissions, UserRole } from '@guardian/interfaces'; export interface NavbarMenuItem { title: string; childItems?: NavbarMenuItem[]; - iconUrl?: string; - svgIcon?: string; + /** PrimeIcons class for the item, e.g. 'pi-wallet'. Inherits currentColor. */ + icon?: string; + /** Optional uppercase group label rendered above this item (vertical layout). */ + section?: string; routerLink?: string; active?: boolean; allowedUserRoles?: UserRole[]; @@ -14,7 +16,8 @@ const NAVBAR_MENU_STANDARD_REGISTRY: NavbarMenuItem[] = [ { title: 'Policies', allowedUserRoles: [UserRole.STANDARD_REGISTRY], - iconUrl: 'table', + icon: 'pi-objects-column', + section: 'Workspace', active: false, childItems: [ { @@ -53,7 +56,7 @@ const NAVBAR_MENU_STANDARD_REGISTRY: NavbarMenuItem[] = [ }, { title: 'Tokens', - iconUrl: 'twoRings', + icon: 'pi-bitcoin', allowedUserRoles: [UserRole.STANDARD_REGISTRY], active: false, childItems: [ @@ -69,7 +72,7 @@ const NAVBAR_MENU_STANDARD_REGISTRY: NavbarMenuItem[] = [ }, { title: 'Relayer Accounts', - svgIcon: 'wallet', + icon: 'pi-wallet', allowedUserRoles: [UserRole.STANDARD_REGISTRY], active: false, routerLink: '/relayer-accounts' @@ -78,7 +81,8 @@ const NAVBAR_MENU_STANDARD_REGISTRY: NavbarMenuItem[] = [ title: 'Administration', allowedUserRoles: [UserRole.STANDARD_REGISTRY], active: false, - iconUrl: 'stars', + icon: 'pi-sliders-h', + section: 'Administration', childItems: [ { title: 'Manage Roles', @@ -117,12 +121,12 @@ const NAVBAR_MENU_AUDITOR: NavbarMenuItem[] = [ title: 'Audit', allowedUserRoles: [UserRole.AUDITOR], active: false, - iconUrl: 'guard', + icon: 'pi-shield', routerLink: '/audit' }, { title: 'Trust Chain', - iconUrl: 'twoRings', + icon: 'pi-sitemap', allowedUserRoles: [UserRole.AUDITOR], active: false, routerLink: '/trust-chain' @@ -228,7 +232,8 @@ function customMenu(user: UserPermissions): NavbarMenuItem[] { menu.push({ title: 'Policies', allowedUserRoles: [UserRole.STANDARD_REGISTRY], - iconUrl: 'table', + icon: 'pi-objects-column', + section: 'Workspace', active: false, childItems }); @@ -280,7 +285,7 @@ function customMenu(user: UserPermissions): NavbarMenuItem[] { } menu.push({ title: 'Tokens', - iconUrl: 'twoRings', + icon: 'pi-bitcoin', allowedUserRoles: [UserRole.STANDARD_REGISTRY], active: false, childItems @@ -346,7 +351,8 @@ function customMenu(user: UserPermissions): NavbarMenuItem[] { title: 'Administration', allowedUserRoles: [UserRole.STANDARD_REGISTRY], active: false, - iconUrl: 'stars', + icon: 'pi-sliders-h', + section: 'Administration', childItems }); } diff --git a/frontend/src/app/views/new-header/new-header.component.html b/frontend/src/app/views/new-header/new-header.component.html index f986c2d080..3741009303 100644 --- a/frontend/src/app/views/new-header/new-header.component.html +++ b/frontend/src/app/views/new-header/new-header.component.html @@ -1,196 +1,190 @@ @if (isLogin) { -