WEB-492: Material Design 3 implementation: Login

This commit is contained in:
Jose Alberto Hernandez
2026-01-04 14:06:28 -05:00
parent 5af11fe757
commit df7274626c
58 changed files with 3396 additions and 855 deletions
+58 -40
View File
@@ -1,41 +1,59 @@
<div class="layout-column m-b-20 f12" ngClass="{{ styleClass }}" id="footer" *ngIf="displayBackEndInfo">
<mat-divider class="divider align-center"></mat-divider>
<div class="layout-column m-b-20 content-wrapper footer-center">
<table class="versions">
<tr>
<td class="footer-content">Tenant</td>
<td class="right footer-content">
{{ tenant | titlecase }}
</td>
</tr>
<tr>
<td class="footer-content">Mifos</td>
<td class="right footer-content">
{{ versions.mifos }} - <b>{{ hash }}</b>
</td>
</tr>
<tr>
<td class="footer-content">{{ 'labels.text.Fineract' | translate }}</td>
<td class="right footer-content">
{{ versions.fineract.version }} - <b>{{ versions.fineract.hash }}</b>
</td>
</tr>
<tr *ngIf="displayBackEndInfo">
<td class="footer-content">{{ 'labels.inputs.Server' | translate }}</td>
<td class="right footer-content">
{{ server }}
</td>
</tr>
<tr *ngIf="isBusinessDateDefined">
<td class="footer-content">{{ 'labels.text.Current Business Date' | translate }}:</td>
<td class="center footer-content business-date">
<b>{{ businessDate | date: 'EEEE, MMMM dd, y' }}</b>
</td>
</tr>
</table>
@if (variant === 'compact') {
<!-- Compact mode for Login -->
<div class="footer-compact">
@if (displayBackEndInfo) {
<div class="footer-info">
<span class="info-item">
<span class="info-label">Mifos:</span>
<span class="info-value">{{ versions.mifos }}</span>
</span>
<span class="info-separator">•</span>
<span class="info-item">
<span class="info-label">{{ 'labels.text.Fineract' | translate }}:</span>
<span class="info-value">{{ versions.fineract.version }}</span>
</span>
<span class="info-separator">•</span>
<span class="info-item">
<span class="info-label">{{ 'labels.inputs.Server' | translate }}:</span>
<span class="info-value">{{ server }}</span>
</span>
</div>
}
</div>
<div class="layout-column m-b-20 copy-label">
<p class="footer-content align-center">{{ 'APP_NAME' | translate }} {{ 'labels.text.by' | translate }} Mifos</p>
</div>
</div>
} @else {
<!-- Default mode for Shell -->
@if (displayBackEndInfo) {
<div class="layout-column m-b-20 f12" ngClass="{{ styleClass }}" id="footer">
<div class="layout-column m-t-10 m-b-10 content-wrapper footer-center">
<table class="versions">
<tr>
<td class="footer-content">Mifos</td>
<td class="right footer-content">
{{ versions.mifos }} - <b>{{ hash }}</b>
</td>
</tr>
<tr>
<td class="footer-content">{{ 'labels.text.Fineract' | translate }}</td>
<td class="right footer-content">
{{ versions.fineract.version }} - <b>{{ versions.fineract.hash }}</b>
</td>
</tr>
<tr>
<td class="footer-content">{{ 'labels.inputs.Server' | translate }}</td>
<td class="right footer-content">
{{ server }}
</td>
</tr>
@if (isBusinessDateDefined) {
<tr>
<td class="footer-content">{{ 'labels.text.Current Business Date' | translate }}:</td>
<td class="center footer-content business-date">
<b>{{ businessDate | date: 'EEEE, MMMM dd, y' }}</b>
</td>
</tr>
}
</table>
</div>
</div>
}
}
+69 -1
View File
@@ -1,6 +1,58 @@
@use 'assets/styles/helper';
@use 'assets/styles/colours' as *;
// ===== Compact Mode (Login) =====
.footer-compact {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;
width: 100%;
font-size: 0.75rem;
color: var(--md-sys-color-on-surface-variant, #44474e);
.footer-info {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 0.5rem;
line-height: 1.4;
}
.info-item {
display: inline-flex;
align-items: center;
gap: 0.25rem;
}
.info-label {
font-weight: 500;
opacity: 0.8;
}
.info-value {
font-weight: 400;
opacity: 0.9;
}
.info-separator {
opacity: 0.4;
font-size: 0.875rem;
}
.footer-copyright {
font-size: 0.875rem;
opacity: 0.7;
margin-top: 0.25rem;
}
}
// Dark theme support for compact mode
.dark-theme .footer-compact {
color: var(--md-sys-color-on-surface-variant, #c4c6d0);
}
// ===== Default Mode (Shell) =====
#footer {
align-items: center;
min-width: 400px;
@@ -48,5 +100,21 @@
.copy-label {
min-width: 100%;
text-align: center;
font-size: large;
font-size: medium;
margin-bottom: 10px;
}
// ===== Responsive for Compact Mode =====
@media (width <= 480px) {
.footer-compact {
font-size: 0.7rem;
.footer-info {
gap: 0.375rem;
}
.footer-copyright {
font-size: 0.8rem;
}
}
}
+1 -2
View File
@@ -12,7 +12,6 @@ import { VersionService } from 'app/system/version.service';
import { environment } from '../../../environments/environment';
import { Subscription } from 'rxjs';
import { NgClass, DatePipe } from '@angular/common';
import { MatDivider } from '@angular/material/divider';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
/**
@@ -25,7 +24,6 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
imports: [
...STANDALONE_SHARED_IMPORTS,
NgClass,
MatDivider,
DatePipe
]
})
@@ -38,6 +36,7 @@ export class FooterComponent implements OnInit, OnDestroy {
private versionService = inject(VersionService);
@Input() styleClass: string = '';
@Input() variant: 'default' | 'compact' = 'default';
/** Mifos X version. */
versions: any = {
+23 -1
View File
@@ -43,6 +43,7 @@ import {
faFileUpload,
faFileWord,
faFillDrip,
faGlobe,
faHandHoldingUsd,
faHome,
faKey,
@@ -113,7 +114,17 @@ import {
faPause,
faReceipt,
faTableCells,
faSave
faSave,
faExternalLinkAlt,
faHeart,
faBookOpen,
faCode,
faLaptopCode,
faComments,
faLightbulb,
faCodeBranch,
faHandHoldingHand,
faHandHoldingHeart
} from '@fortawesome/free-solid-svg-icons';
/**
@@ -137,6 +148,7 @@ export class IconsModule {
faBars,
faBell,
faBook,
faBookOpen,
faBriefcase,
faBuilding,
faCalculator,
@@ -150,9 +162,12 @@ export class IconsModule {
faChevronLeft,
faChevronRight,
faCircle,
faComments,
faHeartBroken,
faClock,
faCloud,
faCode,
faCodeBranch,
faCog,
faCogs,
faCommentAlt,
@@ -163,6 +178,7 @@ export class IconsModule {
faExchange,
faExclamationCircle,
faExclamationTriangle,
faExternalLinkAlt,
faEye,
faEyeSlash,
faFile,
@@ -173,9 +189,15 @@ export class IconsModule {
faFileExcel,
faFileExport,
faFillDrip,
faGlobe,
faHandHoldingHand,
faHandHoldingHeart,
faHandHoldingUsd,
faHeart,
faHome,
faKey,
faLaptopCode,
faLightbulb,
faLink,
faList,
faListUl,
@@ -1,6 +1,13 @@
<mat-form-field id="language-selector" class="m-l-10">
<mat-label>{{ 'labels.inputs.Language' | translate }}</mat-label>
<mat-select [formControl]="languageSelector" (selectionChange)="setLanguage()" class="languageselector">
<mat-form-field id="language-selector" [appearance]="appearance" [class.outlined-variant]="appearance === 'outline'">
@if (showLabel) {
<mat-label>{{ 'labels.inputs.Language' | translate }}</mat-label>
}
<mat-select
[formControl]="languageSelector"
(selectionChange)="setLanguage()"
class="languageselector"
[placeholder]="!showLabel ? ('labels.inputs.Language' | translate) : ''"
>
@for (language of languages; track language) {
<mat-option [value]="language">
{{ 'languages.' + language | translate }}
@@ -1,19 +1,131 @@
::ng-deep .mat-mdc-form-field-bottom-align::before {
border-bottom-color: white;
// ===== Base Language Selector Styles =====
#language-selector {
&.mat-mdc-form-field {
// Default styles (fill variant)
&:not(.outlined-variant) {
margin-left: 10px;
.mat-mdc-text-field-wrapper {
padding-bottom: 0;
}
}
// Outlined variant styles (used in login)
&.outlined-variant {
.mat-mdc-text-field-wrapper {
padding: 0;
}
.mat-mdc-form-field-flex {
align-items: center;
}
.mat-mdc-form-field-infix {
padding: 12px 0;
min-height: 48px;
}
// Outlined border styles with Material Design 3
.mdc-notched-outline {
.mdc-notched-outline__leading,
.mdc-notched-outline__notch,
.mdc-notched-outline__trailing {
border-color: var(--md-sys-color-outline, #79747e);
border-width: 1px;
}
}
// Hover state
&:hover:not(.mat-form-field-disabled) {
.mdc-notched-outline {
.mdc-notched-outline__leading,
.mdc-notched-outline__notch,
.mdc-notched-outline__trailing {
border-color: var(--md-sys-color-on-surface, #1a1c1e);
}
}
}
// Focused state
&.mat-focused {
.mdc-notched-outline {
.mdc-notched-outline__leading,
.mdc-notched-outline__notch,
.mdc-notched-outline__trailing {
border-color: var(--md-sys-color-primary, #1074b9);
border-width: 2px;
}
}
.mat-mdc-floating-label {
color: var(--md-sys-color-primary, #1074b9);
}
}
// Select styling
.mat-mdc-select {
font-size: 0.875rem;
color: var(--md-sys-color-on-surface, #1a1c1e);
}
.mat-mdc-select-arrow {
color: var(--md-sys-color-on-surface-variant, #44474e);
}
// Label styling
.mat-mdc-floating-label {
font-size: 0.875rem;
color: var(--md-sys-color-on-surface-variant, #44474e);
}
}
}
}
::ng-deep .mdc-line-ripple::before {
border-bottom-color: white;
}
// Dark theme support for outlined variant
.dark-theme {
#language-selector.outlined-variant {
.mdc-notched-outline {
.mdc-notched-outline__leading,
.mdc-notched-outline__notch,
.mdc-notched-outline__trailing {
border-color: var(--md-sys-color-outline, #938f99);
}
}
::ng-deep .mdc-line-ripple::after {
border-bottom-color: white;
}
&:hover:not(.mat-form-field-disabled) {
.mdc-notched-outline {
.mdc-notched-outline__leading,
.mdc-notched-outline__notch,
.mdc-notched-outline__trailing {
border-color: var(--md-sys-color-on-surface, #e2e2e5);
}
}
}
::ng-deep .mat-mdc-form-field .mdc-line-ripple::before {
border-bottom-color: white !important;
}
&.mat-focused {
.mdc-notched-outline {
.mdc-notched-outline__leading,
.mdc-notched-outline__notch,
.mdc-notched-outline__trailing {
border-color: var(--md-sys-color-primary, #5ba2ec);
}
}
::ng-deep .mat-mdc-form-field .mdc-line-ripple::after {
border-bottom-color: white !important;
.mat-mdc-floating-label {
color: var(--md-sys-color-primary, #5ba2ec);
}
}
.mat-mdc-select {
color: var(--md-sys-color-on-surface, #e2e2e5);
}
.mat-mdc-select-arrow {
color: var(--md-sys-color-on-surface-variant, #c4c6d0);
}
.mat-mdc-floating-label {
color: var(--md-sys-color-on-surface-variant, #c4c6d0);
}
}
}
@@ -1,5 +1,5 @@
/** Angular Imports */
import { Component, inject } from '@angular/core';
import { Component, inject, Input } from '@angular/core';
import { UntypedFormControl, ReactiveFormsModule } from '@angular/forms';
/** Custom Services */
@@ -25,6 +25,12 @@ export class LanguageSelectorComponent {
private translateService = inject(TranslateService);
private settingsService = inject(SettingsService);
/** Appearance of the form field (fill, outline). Defaults to 'fill'. */
@Input() appearance: 'fill' | 'outline' = 'fill';
/** Show label in the form field. Defaults to true. */
@Input() showLabel: boolean = true;
/** Language selector form control. */
languageSelector = new UntypedFormControl();
+8
View File
@@ -0,0 +1,8 @@
/**
* Material Design 3 UI Components
*
* This module exports all Material Design 3 components for easy importing.
*/
export { M3ButtonComponent, M3ButtonVariant } from './m3-button/m3-button.component';
export { M3IconComponent, M3IconStyle } from './m3-icon/m3-icon.component';
@@ -0,0 +1,31 @@
<div [class]="containerClasses">
@if (variant === 'filled') {
<md-filled-button [type]="type" [disabled]="disabled" (click)="onClick($event)">
{{ label }}<ng-content></ng-content>
</md-filled-button>
}
@if (variant === 'outlined') {
<md-outlined-button [type]="type" [disabled]="disabled" (click)="onClick($event)">
{{ label }}<ng-content></ng-content>
</md-outlined-button>
}
@if (variant === 'elevated') {
<md-elevated-button [type]="type" [disabled]="disabled" (click)="onClick($event)">
{{ label }}<ng-content></ng-content>
</md-elevated-button>
}
@if (variant === 'text') {
<md-text-button [type]="type" [disabled]="disabled" (click)="onClick($event)">
{{ label }}<ng-content></ng-content>
</md-text-button>
}
@if (variant === 'tonal') {
<md-filled-tonal-button [type]="type" [disabled]="disabled" (click)="onClick($event)">
{{ label }}<ng-content></ng-content>
</md-filled-tonal-button>
}
</div>
@@ -0,0 +1,20 @@
// Material Web Button Component Styles
// Base styles are applied globally in src/theme/_material-web.scss
.m3-button-container {
display: inline-block;
&--full-width {
display: block;
width: 100%;
md-filled-button,
md-outlined-button,
md-elevated-button,
md-text-button,
md-filled-tonal-button {
color: var(--md-filled-button-disabled-label-text-color, #44474e);
width: 100%;
}
}
}
@@ -0,0 +1,153 @@
import { Component, Input, Output, EventEmitter, CUSTOM_ELEMENTS_SCHEMA, ViewEncapsulation } from '@angular/core';
import { CommonModule } from '@angular/common';
// Import Material Web Components
import '@material/web/button/filled-button.js';
import '@material/web/button/outlined-button.js';
import '@material/web/button/elevated-button.js';
import '@material/web/button/text-button.js';
import '@material/web/button/filled-tonal-button.js';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
export type M3ButtonVariant = 'filled' | 'outlined' | 'elevated' | 'text' | 'tonal';
/**
* Material Design 3 Button Component
*
* Wrapper for @material/web button components following Material Design 3 specifications.
* Uses native Material Design 3 web components with full theming support.
*
* Features:
* - Five button variants: filled, outlined, elevated, text, and tonal
* - Automatic light/dark theme support
* - Proper disabled states with gray colors
* - Hover, focus, and pressed states with ripple effects
* - Full width support
* - Icon support (leading and trailing)
*
* @example
* // Basic usage with label input
* <mifosx-m3-button
* variant="filled"
* [label]="'Submit'"
* (clicked)="onSubmit()">
* </mifosx-m3-button>
*
* @example
* // With content projection
* <mifosx-m3-button variant="outlined" (clicked)="onCancel()">
* Cancel
* </mifosx-m3-button>
*
* @example
* // Full width disabled button
* <mifosx-m3-button
* variant="filled"
* [label]="'Loading...'"
* [disabled]="true"
* [fullWidth]="true">
* </mifosx-m3-button>
*
* @example
* // With translated label
* <mifosx-m3-button
* variant="tonal"
* type="submit"
* [label]="'labels.buttons.Save' | translate"
* (clicked)="onSave()">
* </mifosx-m3-button>
*/
@Component({
selector: 'mifosx-m3-button',
templateUrl: './m3-button.component.html',
styleUrls: ['./m3-button.component.scss'],
standalone: true,
imports: [
STANDALONE_SHARED_IMPORTS,
CommonModule
],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
encapsulation: ViewEncapsulation.None
})
export class M3ButtonComponent {
/**
* Button variant
* - filled: Solid background (default, highest emphasis)
* - outlined: Bordered button (medium emphasis)
* - elevated: Raised button with shadow (medium emphasis)
* - text: Text-only button (low emphasis)
* - tonal: Subtle background button (medium emphasis)
*/
@Input() variant: M3ButtonVariant = 'filled';
/**
* Button type attribute
*/
@Input() type: 'button' | 'submit' | 'reset' = 'button';
/**
* Disabled state
*/
@Input() disabled: boolean = false;
/**
* Button label text
*/
@Input() label?: string;
/**
* Icon to display before text
*/
@Input() icon?: string;
/**
* Icon to display after text
*/
@Input() trailingIcon?: string;
/**
* Whether the button should take full width
*/
@Input() fullWidth: boolean = false;
/**
* Click event emitter
*/
@Output() clicked = new EventEmitter<Event>();
/**
* Handle button click
*/
onClick(event: Event): void {
if (!this.disabled) {
this.clicked.emit(event);
}
}
/**
* Get the Material Web Component tag name based on variant
*/
get componentTag(): string {
switch (this.variant) {
case 'filled':
return 'md-filled-button';
case 'outlined':
return 'md-outlined-button';
case 'elevated':
return 'md-elevated-button';
case 'text':
return 'md-text-button';
case 'tonal':
return 'md-filled-tonal-button';
default:
return 'md-filled-button';
}
}
/**
* Get container classes
*/
get containerClasses(): string {
return this.fullWidth ? 'm3-button-container--full-width' : 'm3-button-container';
}
}
@@ -0,0 +1 @@
<span [class]="iconClass" [ngStyle]="iconStyles" class="m3-icon">{{ name }}</span>
@@ -0,0 +1,71 @@
// Material Symbols Font Faces
// These will be loaded from Google Fonts or local files
@use 'theme/material-web' as *;
.m3-icon {
display: inline-flex;
align-items: center;
justify-content: center;
user-select: none;
vertical-align: middle;
// Smooth rendering
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizelegibility;
}
// Material Symbols base styles
.material-symbols-outlined,
.material-symbols-filled,
.material-symbols-rounded,
.material-symbols-sharp {
font-family: 'Material Symbols Outlined', sans-serif;
font-weight: normal;
font-style: normal;
line-height: 1;
letter-spacing: normal;
text-transform: none;
white-space: nowrap;
word-wrap: normal;
direction: ltr;
color: var(--md-icon-prefix-label-text-color, #44474e);
}
// Filled variant
.material-symbols-filled {
font-variation-settings:
'FILL' 1,
'wght' 400,
'GRAD' 0,
'opsz' 24;
}
// Outlined variant (default)
.material-symbols-outlined {
font-variation-settings:
'FILL' 0,
'wght' 400,
'GRAD' 0,
'opsz' 24;
}
// Rounded variant
.material-symbols-rounded {
font-family: 'Material Symbols Rounded', sans-serif;
font-variation-settings:
'FILL' 0,
'wght' 400,
'GRAD' 0,
'opsz' 24;
}
// Sharp variant
.material-symbols-sharp {
font-family: 'Material Symbols Sharp', sans-serif;
font-variation-settings:
'FILL' 0,
'wght' 400,
'GRAD' 0,
'opsz' 24;
}
@@ -0,0 +1,95 @@
import { Component, Input, ViewEncapsulation } from '@angular/core';
import { CommonModule } from '@angular/common';
export type M3IconStyle = 'outlined' | 'filled' | 'rounded' | 'sharp';
/**
* Material Design 3 Icon Component
*
* Wrapper for Material Symbols icons following Material Design 3 specifications.
* Uses Google's Material Symbols font for consistent iconography.
*
* Features:
* - Multiple icon styles: outlined (default), filled, rounded, and sharp
* - Automatic theme support
* - Size control
* - Color customization via CSS
*
* @example
* // Basic usage
* <mifosx-m3-icon name="account_circle"></mifosx-m3-icon>
*
* @example
* // With filled style
* <mifosx-m3-icon name="lock" style="filled"></mifosx-m3-icon>
*
* @example
* // With custom size
* <mifosx-m3-icon name="visibility" [size]="24"></mifosx-m3-icon>
*
* Common icon mappings from Font Awesome:
* - user-circle → account_circle
* - lock → lock
* - eye → visibility
* - eye-slash → visibility_off
* - home → home
* - search → search
* - settings → settings
* - menu → menu
* - close → close
*/
@Component({
selector: 'mifosx-m3-icon',
templateUrl: './m3-icon.component.html',
styleUrls: ['./m3-icon.component.scss'],
standalone: true,
imports: [CommonModule],
encapsulation: ViewEncapsulation.None
})
export class M3IconComponent {
/**
* Icon name from Material Symbols
* See: https://fonts.google.com/icons
*/
@Input() name!: string;
/**
* Icon style variant
* - outlined: Default style with strokes (default)
* - filled: Solid filled icons
* - rounded: Rounded corner icons
* - sharp: Sharp corner icons
*/
@Input() style: M3IconStyle = 'outlined';
/**
* Icon size in pixels
* Default is 24px (Material Design standard)
*/
@Input() size: number = 24;
/**
* Whether the icon should be filled (alternative to style="filled")
* This is a convenience property for backwards compatibility
*/
@Input() filled: boolean = false;
/**
* Get the CSS class for the icon style
*/
get iconClass(): string {
const styleToUse = this.filled ? 'filled' : this.style;
return `material-symbols-${styleToUse}`;
}
/**
* Get the inline styles for the icon
*/
get iconStyles(): { [key: string]: string } {
return {
'font-size': `${this.size}px`,
width: `${this.size}px`,
height: `${this.size}px`
};
}
}
@@ -1,11 +1,14 @@
@if (existMoreThanOneServer) {
<mat-form-field id="server-selector">
<mat-label>{{ 'labels.inputs.Server' | translate }}</mat-label>
<mat-form-field id="server-selector" [appearance]="appearance" [class.outlined-variant]="appearance === 'outline'">
@if (showLabel) {
<mat-label>{{ 'labels.inputs.Server' | translate }}</mat-label>
}
<mat-select
[formControl]="serverSelector"
(selectionChange)="setServer()"
panelClass="server-panel"
panelWidth="null"
[placeholder]="!showLabel ? ('labels.inputs.Server' | translate) : ''"
>
<div class="sticky">
<form [formGroup]="form" (ngSubmit)="addNewServer()">
@@ -1,3 +1,138 @@
// ===== Base Server Selector Styles =====
#server-selector {
&.mat-mdc-form-field {
// Default styles (fill variant)
&:not(.outlined-variant) {
mat-form-field {
width: 96%;
}
}
// Outlined variant styles (used in login)
&.outlined-variant {
.mat-mdc-text-field-wrapper {
padding: 0;
}
.mat-mdc-form-field-flex {
align-items: center;
}
.mat-mdc-form-field-infix {
padding: 12px 0;
min-height: 48px;
}
// Outlined border styles with Material Design 3
.mdc-notched-outline {
.mdc-notched-outline__leading,
.mdc-notched-outline__notch,
.mdc-notched-outline__trailing {
border-color: var(--md-sys-color-outline, #79747e);
border-width: 1px;
}
}
// Hover state
&:hover:not(.mat-form-field-disabled) {
.mdc-notched-outline {
.mdc-notched-outline__leading,
.mdc-notched-outline__notch,
.mdc-notched-outline__trailing {
border-color: var(--md-sys-color-on-surface, #1a1c1e);
}
}
}
// Focused state
&.mat-focused {
.mdc-notched-outline {
.mdc-notched-outline__leading,
.mdc-notched-outline__notch,
.mdc-notched-outline__trailing {
border-color: var(--md-sys-color-primary, #1074b9);
border-width: 2px;
}
}
.mat-mdc-floating-label {
color: var(--md-sys-color-primary, #1074b9);
}
}
// Select styling
.mat-mdc-select {
font-size: 0.875rem;
color: var(--md-sys-color-on-surface, #1a1c1e);
}
.mat-mdc-select-arrow {
color: var(--md-sys-color-on-surface-variant, #44474e);
}
// Label styling
.mat-mdc-floating-label {
font-size: 0.875rem;
color: var(--md-sys-color-on-surface-variant, #44474e);
}
}
}
}
// Dark theme support for outlined variant
.dark-theme {
#server-selector.outlined-variant {
.mdc-notched-outline {
.mdc-notched-outline__leading,
.mdc-notched-outline__notch,
.mdc-notched-outline__trailing {
border-color: var(--md-sys-color-outline, #938f99);
}
}
&:hover:not(.mat-form-field-disabled) {
.mdc-notched-outline {
.mdc-notched-outline__leading,
.mdc-notched-outline__notch,
.mdc-notched-outline__trailing {
border-color: var(--md-sys-color-on-surface, #e2e2e5);
}
}
}
&.mat-focused {
.mdc-notched-outline {
.mdc-notched-outline__leading,
.mdc-notched-outline__notch,
.mdc-notched-outline__trailing {
border-color: var(--md-sys-color-primary, #5ba2ec);
}
}
.mat-mdc-floating-label {
color: var(--md-sys-color-primary, #5ba2ec);
}
}
.mat-mdc-select {
color: var(--md-sys-color-on-surface, #e2e2e5);
}
.mat-mdc-select-arrow {
color: var(--md-sys-color-on-surface-variant, #c4c6d0);
}
.mat-mdc-floating-label {
color: var(--md-sys-color-on-surface-variant, #c4c6d0);
}
.sticky {
background-color: var(--md-sys-color-surface-container, #2c2f36);
}
}
}
// ===== Common Styles =====
.actions {
display: flex;
align-items: center;
@@ -12,8 +147,8 @@
position: sticky;
z-index: 1; // appear above mat-option
top: 0; // fix at the top
background-color: white; // don't see mat-option when scrolling
box-shadow: 1px -2px 10px 0 grey; // shadow at the bottom
background-color: var(--md-sys-color-surface-container, white); // don't see mat-option when scrolling
box-shadow: 1px -2px 10px 0 rgb(0 0 0 / 10%); // shadow at the bottom
}
mat-form-field {
@@ -1,5 +1,5 @@
/** Angular Imports */
import { Component, OnInit, inject } from '@angular/core';
import { Component, OnInit, inject, Input } from '@angular/core';
import { UntypedFormControl, ReactiveFormsModule } from '@angular/forms';
import { MatDialog } from '@angular/material/dialog';
import { UntypedFormBuilder, Validators } from '@angular/forms';
@@ -27,6 +27,12 @@ export class ServerSelectorComponent implements OnInit {
dialog = inject(MatDialog);
private formBuilder = inject(UntypedFormBuilder);
/** Appearance of the form field (fill, outline). Defaults to 'fill'. */
@Input() appearance: 'fill' | 'outline' = 'fill';
/** Show label in the form field. Defaults to true. */
@Input() showLabel: boolean = true;
/** Input server. */
form: any;
@@ -1,7 +1,4 @@
<mat-form-field class="flex-fill align-center" id="tenant-selector">
<span matPrefix class="m-r-10">
<fa-icon icon="building" size="lg"></fa-icon>
</span>
<mat-form-field class="flex-fill align-center" id="tenant-selector" appearance="fill">
<mat-label>{{ 'labels.inputs.Tenant' | translate }}</mat-label>
<mat-select [formControl]="tenantSelector" (selectionChange)="setTenantIdentifier()" class="tenantselector">
@for (tenant of tenants; track tenant) {
@@ -10,4 +7,5 @@
</mat-option>
}
</mat-select>
<mifosx-m3-icon matIconPrefix name="domain"></mifosx-m3-icon>
</mat-form-field>
@@ -1,5 +1,2 @@
#tenant-selector,
.tenantselector {
width: 14rem;
margin-bottom: 0.6rem;
}
// Styles are now controlled by parent component (login-form)
// to ensure consistency with other form fields
@@ -3,7 +3,7 @@ import { UntypedFormControl, ReactiveFormsModule } from '@angular/forms';
import { SettingsService } from 'app/settings/settings.service';
import { AlertService } from 'app/core/alert/alert.service';
import { MatFormField, MatPrefix, MatLabel } from '@angular/material/form-field';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { M3IconComponent } from 'app/shared/m3-ui/m3-icon/m3-icon.component';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@Component({
@@ -13,7 +13,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
imports: [
...STANDALONE_SHARED_IMPORTS,
MatPrefix,
FaIconComponent
M3IconComponent
]
})
export class TenantSelectorComponent implements OnInit {
@@ -1,3 +1,3 @@
<button mat-icon-button (click)="toggleTheme()" class="m-l-15 m-r-5">
<mat-icon>{{ darkModeOn ? 'wb_sunny' : 'nights_stay' }}</mat-icon>
<button mat-icon-button (click)="toggleTheme()" class="m-l-5 m-r-5">
<mifosx-m3-icon [name]="darkModeOn ? 'wb_sunny' : 'nights_stay'"></mifosx-m3-icon>
</button>
@@ -2,7 +2,7 @@ import { Component, OnChanges, OnInit, SimpleChanges, inject } from '@angular/co
import { ThemingService } from './theming.service';
import { SettingsService } from 'app/settings/settings.service';
import { MatIconButton } from '@angular/material/button';
import { MatIcon } from '@angular/material/icon';
import { M3IconComponent } from '../m3-ui/m3-icon/m3-icon.component';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@Component({
@@ -12,7 +12,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
imports: [
...STANDALONE_SHARED_IMPORTS,
MatIconButton,
MatIcon
M3IconComponent
]
})
export class ThemeToggleComponent implements OnInit, OnChanges {
@@ -29,8 +29,16 @@ export class ThemeToggleComponent implements OnInit, OnChanges {
this.darkModeOn = !!this.settingsService.themeDarkEnabled;
}
/**
* Toggle between light and dark themes
* This method handles the complete theme switching process:
* 1. Toggles the local state
* 2. Persists the preference to settings
*/
toggleTheme() {
// Step 1: Toggle the dark mode state
this.darkModeOn = !this.darkModeOn;
// Step 2: Persist the theme preference to localStorage via settings service
this.settingsService.setThemeDarkEnabled(this.darkModeOn);
this.themingService.setDarkMode(this.darkModeOn);
}
@@ -41,8 +41,10 @@ export class ThemingService {
this.darkModeOn = isDarkMode;
if (isDarkMode) {
document.body.classList.add('dark-theme');
document.body.classList.remove('light-theme');
this.theme.next('dark-theme');
} else {
document.body.classList.add('light-theme');
document.body.classList.remove('dark-theme');
this.theme.next('light-theme');
}