WEB-492: Material Design 3 implementation: Login
This commit is contained in:
@@ -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>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user