new design implement for login screen, organization onboarding form changes, new filter implement in all screens, remove icons from validation,

This commit is contained in:
Gagan7900
2026-07-29 19:12:51 +05:30
parent 0907e0f5dc
commit 4838e16dc8
40 changed files with 2407 additions and 793 deletions
+101 -15
View File
@@ -1,36 +1,75 @@
:host {
display: inline-flex;
align-items: center;
vertical-align: middle;
&.w-full {
display: flex;
width: 100%;
}
}
:host-context(.modern-modal-footer) button.w-full,
:host-context(.onboarding-action-footer) button.w-full {
width: 100% !important;
}
:host-context(.modern-modal-footer) button,
:host-context(.onboarding-action-footer) button {
height: 36px;
padding: 0 16px;
border-radius: 10px;
font-size: 13px;
font-weight: 600;
transition: .3s ease;
height: 36px !important;
min-height: 36px !important;
max-height: 36px !important;
padding: 0 16px !important;
border-radius: 10px !important;
font-size: 13px !important;
line-height: 1 !important;
font-weight: 600 !important;
transition: .3s ease !important;
box-sizing: border-box !important;
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
vertical-align: middle !important;
margin: 0 !important;
}
:host-context(.modern-modal-footer) button.ti-btn-light,
:host-context(.onboarding-action-footer) button.ti-btn-light {
border: 1px solid #dbe3ef;
border: 1.5px solid #dbe3ef !important;
background: #ffffff;
color: #475569;
:host-context(.dark) &,
:host-context([data-theme-mode="dark"]) & {
border-color: rgba(255, 255, 255, 0.15) !important;
background: rgba(255, 255, 255, 0.05) !important;
color: #cbd5e1 !important;
}
}
:host-context(.modern-modal-footer) button.ti-btn-light:hover:not(:disabled),
:host-context(.onboarding-action-footer) button.ti-btn-light:hover:not(:disabled) {
background: #f8fafc;
border-color: #cbd5e1;
border-color: #cbd5e1 !important;
transform: translateY(-2px);
:host-context(.dark) &,
:host-context([data-theme-mode="dark"]) & {
background: rgba(255, 255, 255, 0.12) !important;
border-color: rgba(255, 255, 255, 0.25) !important;
color: #ffffff !important;
}
}
:host-context(.modern-modal-footer) button.ti-btn-primary-full,
:host-context(.modern-modal-footer) button.ti-btn-success-full {
position: relative;
overflow: hidden;
border: none;
border: 1.5px solid transparent !important;
color: #ffffff;
background: #7c3deb;
box-shadow: 0 8px 20px rgba(125, 61, 235, 0.468);
transition: .35s ease;
transition: .35s ease !important;
}
:host-context(.modern-modal-footer) button.ti-btn-primary-full:hover:not(:disabled),
@@ -39,8 +78,35 @@
box-shadow: 0 12px 28px rgba(37, 99, 235, .45);
}
/* ==========================================================================
Single Solid Button Rule using CSS :has()
When an outline button (Cancel/Save Draft) is hovered, demote the default
solid button ('Next') to outline style and remove its glow/box-shadow.
========================================================================== */
:host-context(.modern-modal-footer:has(.ti-btn-outline-primary:hover:not(:disabled))) button.ti-btn-primary-full:not(:hover),
:host-context(.modern-modal-footer:has(.onboarding-save-btn:hover:not(:disabled))) button.ti-btn-primary-full:not(:hover) {
background: #ffffff !important;
color: var(--primary, #7c3deb) !important;
border: 1.5px solid var(--primary, #7c3deb) !important;
box-shadow: none !important;
transform: none !important;
&::before {
opacity: 0 !important;
animation: none !important;
}
:host-context(.dark) &,
:host-context([data-theme-mode="dark"]) & {
background: transparent !important;
color: #c084fc !important;
border-color: #a855f7 !important;
}
}
:host-context(.modern-modal-footer) button.ti-btn-primary-full::before,
:host-context(.modern-modal-footer) button.ti-btn-success-full::before{
:host-context(.modern-modal-footer) button.ti-btn-outline-primary::before,
:host-context(.modern-modal-footer) button.ti-btn-success-full::before {
content: "";
position: absolute;
top: 0;
@@ -60,14 +126,34 @@
}
:host-context(.modern-modal-footer) button.onboarding-nav-btn {
height: 34px;
padding: 0 12px;
font-size: 12px;
height: 36px;
padding: 0 16px;
font-size: 13px;
}
:host-context(.modern-modal-footer) button.onboarding-save-btn {
border: 1.5px solid var(--primary, #7c3deb) !important;
background: #ffffff;
color: var(--primary, #7c3deb);
transition: all 0.35s ease !important;
position: relative;
overflow: hidden;
:host-context(.dark) &,
:host-context([data-theme-mode="dark"]) & {
background: transparent;
color: #c084fc;
border-color: #a855f7;
}
}
:host-context(.modern-modal-footer) button.onboarding-save-btn:hover:not(:disabled) {
background: var(--primary, #7c3deb) !important;
background: linear-gradient(135deg, var(--primary, #7c3deb) 0%, #a855f7 100%) !important;
color: #ffffff !important;
border-color: transparent !important;
transform: translateY(-3px);
box-shadow: 0 12px 28px rgba(37, 99, 235, .45);
box-shadow: 0 12px 28px rgba(124, 61, 235, 0.45);
}
@keyframes saveButtonShine {
@@ -15,6 +15,7 @@ export type IconPosition = 'left' | 'right';
export type ButtonVariant =
| 'primary'
| 'primary-full'
| 'outline-primary'
| 'secondary'
| 'secondary-full'
| 'success'
@@ -201,6 +202,9 @@ const BUTTON_ACTION_CONFIG: Record<ButtonAction, ButtonActionConfig> = {
standalone: true,
templateUrl: './button.html',
styleUrl: './button.scss',
host: {
'[class.w-full]': 'fullWidth()'
},
changeDetection: ChangeDetectionStrategy.OnPush
})
export class Button {
@@ -293,6 +297,7 @@ export class Button {
const variants: Record<ButtonVariant, string> = {
primary: 'ti-btn-primary',
'primary-full': 'ti-btn-primary-full',
'outline-primary': 'ti-btn-outline-primary',
secondary: 'ti-btn-secondary',
'secondary-full': 'ti-btn-secondary-full',
@@ -7,6 +7,13 @@
{{ tableTitle () }}
</h5>
<div class="flex flex-wrap items-center gap-3">
@if(showFilterButton()){
<div animate.enter="filter-btn-enter" animate.leave="filter-btn-leave">
<app-button action="custom" icon="ti ti-filter" label="Filter" size="sm" iconClass="!text-[1rem]"
[variant]="filterActive() ? 'primary-full' : 'outline-primary'"
className="!rounded-full shadow-sm !whitespace-nowrap" (buttonClicked)="onFilterClick($event)" />
</div>
}
@if(showAddButton()){
<div>
<app-button action="add" [label]="buttonTitle()" size="sm" iconClass="!text-[1rem]"
@@ -30,6 +37,26 @@
</div>
</div>
@if (toolbarTemplate(); as toolbar) {
<div class="table-filter-row" [class.is-open]="filterActive()">
<div class="table-filter-row-inner">
<div class="table-filter-row-content" #filterRowContent>
<ng-container [ngTemplateOutlet]="toolbar.templateRef" />
@if (filterActionsTemplate(); as filterActions) {
<div class="table-filter-row-actions" [class.is-pinned]="filterRowOverflowing()">
<ng-container [ngTemplateOutlet]="filterActions.templateRef" />
</div>
}
</div>
<button type="button"
class="ti-btn ti-btn-icon ti-btn-sm ti-btn-light !rounded-full !m-0 shrink-0 filter-close-btn"
aria-label="Close filter" (click)="onFilterClick($event)">
<i class="ti ti-x"></i>
</button>
</div>
</div>
}
<div class="box-body !p-0 modern-table-body">
<div class="table-responsive overflow-x-auto overflow-y-visible modern-table-wrapper">
<table [class]="tableClass()">
@@ -253,4 +253,158 @@
.data-table-modern .custom-floating-input[type='number'] {
-moz-appearance: textfield;
}
/* Filter toggle button (in the toolbar) */
.filter-btn-enter {
animation: filterButtonIn 0.2s ease-out;
}
.filter-btn-leave {
animation: filterButtonOut 0.15s ease-in forwards;
}
@keyframes filterButtonIn {
from {
opacity: 0;
transform: scale(0.9);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes filterButtonOut {
from {
opacity: 1;
transform: scale(1);
}
to {
opacity: 0;
transform: scale(0.9);
}
}
/* Full-width filter row, joined to the table card */
.table-filter-row {
display: grid;
grid-template-rows: 0fr;
border-bottom-width: 0;
background-color: color-mix(in srgb, var(--color-primary) 3%, transparent);
transition: grid-template-rows 0.38s cubic-bezier(0.4, 0, 0.2, 1),
border-bottom-width 0.38s cubic-bezier(0.4, 0, 0.2, 1);
}
.table-filter-row.is-open {
grid-template-rows: 1fr;
border-bottom-width: 1px;
border-bottom-style: solid;
border-bottom-color: var(--color-defaultborder);
}
:host-context(.dark) .table-filter-row {
border-color: color-mix(in srgb, #fff 10%, transparent);
background-color: color-mix(in srgb, #fff 3%, transparent);
}
.table-filter-row-inner {
grid-row: 1;
grid-column: 1;
display: flex;
align-items: flex-start;
gap: 0.75rem;
padding: 0 1.25rem;
min-height: 0;
min-width: 0;
opacity: 0;
transition: opacity 0.28s ease 0.06s, padding 0.38s cubic-bezier(0.4, 0, 0.2, 1);
align-items: baseline;
}
.table-filter-row:not(.is-open) .table-filter-row-inner {
overflow: hidden;
}
.table-filter-row.is-open .table-filter-row-inner {
padding: 0.875rem 1.25rem;
opacity: 1;
transition: opacity 0.32s ease 0.08s, padding 0.38s cubic-bezier(0.4, 0, 0.2, 1);
}
.table-filter-row-content {
flex: 1 1 auto;
display: flex;
flex-wrap: nowrap;
align-items: flex-end;
gap: 0.75rem;
min-width: 0;
overflow-x: auto;
overflow-y: hidden;
padding-bottom: 0.5rem;
margin-bottom: -0.5rem;
padding-top: 10px;
scrollbar-width: thin;
scrollbar-color: color-mix(in srgb, var(--color-primary) 40%, transparent) transparent;
}
.table-filter-row-content>* {
flex: 0 0 auto;
}
.table-filter-row-content::-webkit-scrollbar {
height: 2px;
}
.table-filter-row-content::-webkit-scrollbar-thumb {
background-color: color-mix(in srgb, var(--color-primary) 45%, transparent);
border-radius: 999px;
transition: background-color 0.2s ease;
}
.table-filter-row-content:hover::-webkit-scrollbar-thumb {
background-color: color-mix(in srgb, var(--color-primary) 65%, transparent);
}
.table-filter-row-content::-webkit-scrollbar-track {
background: transparent;
}
.table-filter-row-actions {
display: flex;
align-items: flex-end;
gap: 0.75rem;
flex: 0 0 auto;
}
.table-filter-row-actions.is-pinned {
position: sticky;
right: 0;
margin-left: auto;
padding-left: 0.75rem;
background: linear-gradient(to right, transparent, color-mix(in srgb, var(--color-primary) 3%, var(--color-white)) 1rem);
}
:host-context(.dark) .table-filter-row-actions.is-pinned {
background: linear-gradient(to right, transparent, color-mix(in srgb, #fff 3%, var(--color-bodybg)) 1rem);
}
.table-filter-row .filter-close-btn {
margin-top: 0.125rem;
}
.filter-close-btn {
transition: transform 0.15s ease, background-color 0.15s ease;
border-color: #7c3deb;
}
.filter-close-btn:hover {
transform: rotate(90deg);
}
.filter-close-btn:active {
transform: rotate(90deg) scale(0.9);
}
@@ -1,5 +1,5 @@
import { NgTemplateOutlet } from '@angular/common';
import { Component, DestroyRef, HostListener, computed, contentChildren, effect, inject, input, output, signal } from '@angular/core';
import { Component, DestroyRef, ElementRef, HostListener, computed, contentChildren, effect, inject, input, output, signal, viewChild, contentChild } from '@angular/core';
import { MatPaginatorModule, PageEvent } from '@angular/material/paginator';
import { Subject } from 'rxjs';
import { debounceTime, distinctUntilChanged } from 'rxjs/operators';
@@ -10,9 +10,9 @@ import { CdkConnectedOverlay, CdkOverlayOrigin, ConnectedPosition } from '@angul
import { DataTableAction, DataTableActionEvent, DataTableCellContext, DataTableColumn, DataTablePageEvent, DataTableRecord, DataTableSortEvent } from './data-table.types';
import { Button } from '../button/button';
import { contentChild } from '@angular/core';
import { DataTableToolbarDirective } from '../../directives/data-table-toolbar/data-table-toolbar.directive';
import { DataTableFilterActionsDirective } from '../../directives/data-table-filter-actions/data-table-filter-actions.directive';
@Component({
selector: 'app-data-table',
@@ -34,6 +34,34 @@ export class DataTable<T extends DataTableRecord = DataTableRecord> {
readonly cellTemplates = contentChildren(DataTableCellDirective);
readonly toolbarTemplate = contentChild(DataTableToolbarDirective);
readonly filterActionsTemplate = contentChild(DataTableFilterActionsDirective);
readonly filterRowContent = viewChild<ElementRef<HTMLElement>>('filterRowContent');
readonly filterRowOverflowing = signal(false);
private filterRowResizeObserver?: ResizeObserver;
private readonly filterRowContentEffect = effect(onCleanup => {
const element = this.filterRowContent()?.nativeElement;
this.filterRowResizeObserver?.disconnect();
if (!element) {
this.filterRowOverflowing.set(false);
return;
}
const checkOverflow = () => {
this.filterRowOverflowing.set(element.scrollWidth > element.clientWidth + 1);
};
checkOverflow();
this.filterRowResizeObserver = new ResizeObserver(checkOverflow);
this.filterRowResizeObserver.observe(element);
onCleanup(() => this.filterRowResizeObserver?.disconnect());
});
private readonly defaultRowClasses = [
'table-primary',
'table-secondary',
@@ -110,6 +138,8 @@ export class DataTable<T extends DataTableRecord = DataTableRecord> {
searchDebounceTime = input(300);
emptyMessage = input('No records found');
emptyDescription = input('There is currently no data to display.');
showFilterButton = input<boolean>(false);
filterActive = input<boolean>(false);
/*---------------------------*/
/* --------- Permission inputs ---- */
@@ -141,6 +171,7 @@ export class DataTable<T extends DataTableRecord = DataTableRecord> {
actionHeaderClass = input<string>('!text-center');
actionCellClass = input<string>('!text-center');
addClicked = output<void>();
filterClicked = output<void>();
totalVisibleColumns = computed(() => this.columns().length + (this.actions().length > 0 ? 1 : 0));
@@ -563,6 +594,16 @@ export class DataTable<T extends DataTableRecord = DataTableRecord> {
return this.composeClass(baseClass, '');
}
onFilterClick(event: MouseEvent): void {
event.preventDefault();
event.stopPropagation();
this.closeActionMenu();
this.filterClicked.emit();
}
}
export { DataTableCellDirective } from '../../directives/data-table-cell.directive';
export { DataTableToolbarDirective } from '../../directives/data-table-toolbar/data-table-toolbar.directive';
export { DataTableFilterActionsDirective } from '../../directives/data-table-filter-actions/data-table-filter-actions.directive';
@@ -1,41 +1,39 @@
<app-form-field
[label]="label()"
[inputId]="inputId()"
[control]="control()"
[required]="required()"
[disabled]="isDisabled()"
[description]="description()"
[hint]="hint()"
[help]="help()"
[floating]="isFloating()"
[forceFloat]="shouldFloat()"
[hideValidation]="hideValidation()"
[showValidationWhenDirty]="showValidationWhenDirty()"
[submitAttempted]="submitAttempted()"
<app-form-field [label]="label()" [inputId]="inputId()" [control]="control()" [required]="required()"
[disabled]="isDisabled()" [description]="description()" [hint]="hint()" [help]="help()" [floating]="isFloating()"
[forceFloat]="shouldFloat()" [hideValidation]="hideValidation()"
[showValidationWhenDirty]="showValidationWhenDirty()" [submitAttempted]="submitAttempted()"
[validationMessages]="validationMessages()"
[wrapperClass]="(isFloating() ? 'date-picker-floating ' : '') + wrapperClass()"
[labelClass]="labelClass()"
[contentClass]="fieldContentClass()"
>
<div class="relative w-full">
<input
data-form-control
[id]="inputId()"
type="date"
[class]="inputClass()"
[value]="value() ?? ''"
[placeholder]="isFloating() ? ' ' : placeholder()"
[required]="required()"
[readOnly]="readonly()"
[disabled]="isDisabled()"
[attr.min]="min()"
[attr.max]="max()"
[attr.aria-label]="label()"
[attr.aria-invalid]="showError() ? 'true' : null"
[attr.aria-required]="required() ? 'true' : null"
(focus)="onFocus()"
(input)="onInput($event)"
(blur)="onBlur()"
/>
[wrapperClass]="(isFloating() ? 'date-picker-floating ' : '') + wrapperClass()" [labelClass]="labelClass()"
[contentClass]="fieldContentClass()">
<div #wrapperDiv class="relative w-full flex items-center cursor-pointer" (click)="openCalendar($event)">
<spk-flatpickr #flatpickrComponent [id]="inputId()" [placeholder]="isFloating() ? ' ' : placeholder()" [mode]="mode()"
[minDate]="min() || undefined" [maxDate]="max() || undefined" [readonly]="readonly()" [disabled]="isDisabled()"
[altInput]="false" [convertModelValue]="true" [enableTime]="false" [dateFormat]="'Y-m-d'"
[class]="inputClass() + ' !pe-14 ' + (mode() === 'range' && isRangeSelected() && !focused() ? '!text-transparent' : '')"
[ngModel]="flatpickrValue()" (ngModelChange)="onDateChange($event)" (focus)="onFocus()" (blur)="onBlur()" />
@if (mode() === 'range' && isRangeSelected() && !focused()) {
<div
class="pointer-events-none absolute inset-y-0 start-0 flex items-center ps-3 text-xs sm:text-sm font-medium text-defaulttextcolor dark:text-white/90">
<span>{{ rangeFrom() }}</span>
<span
class="badge bg-primary/10 text-primary dark:bg-primary/20 dark:text-primary-light font-bold text-[0.7rem] uppercase tracking-wider mx-2 px-2 py-0.5 rounded border border-primary/20">
TO
</span>
<span>{{ rangeTo() }}</span>
</div>
}
<div class="absolute inset-y-0 end-0 flex items-center pe-3 gap-1 z-10">
@if (clearable() && hasValue() && !isDisabled() && !readonly()) {
<button #clearBtn type="button"
class="spk-date-picker-clear-btn text-[#8c9097] dark:text-white/50 hover:text-red-500 dark:hover:text-red-400 p-0.5 rounded-full transition-colors cursor-pointer flex items-center justify-center"
(click)="clearDate($event)" aria-label="Clear date">
<i class="ri-close-line text-[1.1rem]"></i>
</button>
}
<span #calendarIcon class="pointer-events-none text-[#8c9097] dark:text-white/50 flex items-center justify-center" aria-hidden="true">
<i class="ri-calendar-line text-[1.1rem]"></i>
</span>
</div>
</div>
</app-form-field>
</app-form-field>
@@ -4,107 +4,82 @@
min-width: 0;
}
:host ::ng-deep .date-picker-floating .floating-label .form-control {
min-height: 2.5rem !important;
height: 2.5rem !important;
border: 1px solid var(--color-inputborder);
border-radius: 0.5rem;
background-color: var(--color-white);
color: transparent !important;
font-size: 0.8125rem;
padding-block: 0.5rem 0.15rem !important;
padding-inline: 0.75rem !important;
box-shadow: none;
transition: border-color 0.2s ease, box-shadow 0.2s ease, color 0.2s ease;
}
:host ::ng-deep {
spk-flatpickr {
display: block;
flex: 1 1 auto;
min-width: 0;
width: 100%;
}
/* Hide native datetime format (dd/mm/yyyy) when un-floated and not focused */
:host ::ng-deep .date-picker-floating .floating-label:not(.floating-label--float):not(:focus-within) .form-control::-webkit-datetime-edit,
:host ::ng-deep .date-picker-floating .floating-label:not(.floating-label--float):not(:focus-within) .form-control::-webkit-datetime-edit-fields-wrapper,
:host ::ng-deep .date-picker-floating .floating-label:not(.floating-label--float):not(:focus-within) .form-control::-webkit-datetime-edit-text,
:host ::ng-deep .date-picker-floating .floating-label:not(.floating-label--float):not(:focus-within) .form-control::-webkit-datetime-edit-month-field,
:host ::ng-deep .date-picker-floating .floating-label:not(.floating-label--float):not(:focus-within) .form-control::-webkit-datetime-edit-day-field,
:host ::ng-deep .date-picker-floating .floating-label:not(.floating-label--float):not(:focus-within) .form-control::-webkit-datetime-edit-year-field {
color: transparent !important;
}
.date-picker-floating .floating-label .form-control {
min-height: 2.5rem !important;
height: 2.5rem !important;
border: 1px solid var(--color-inputborder, #e2e8f0);
border-radius: 0.5rem !important;
background-color: var(--color-white, #ffffff);
color: var(--color-defaulttextcolor, #334155);
font-size: 0.8125rem;
padding-block: 0.5rem 0.15rem !important;
padding-inline: 0.75rem 2.5rem !important;
box-shadow: none;
transition: border-color 0.2s ease, box-shadow 0.2s ease, color 0.2s ease;
}
:host ::ng-deep .date-picker-floating .floating-label > label {
position: absolute !important;
inset-inline-start: 0.75rem !important;
padding-inline: 0.35rem !important;
background-color: var(--color-white) !important;
font-weight: 400 !important;
color: #8c9097 !important;
z-index: 9999 !important;
overflow: visible !important;
white-space: nowrap !important;
pointer-events: none !important;
}
.date-picker-floating .floating-label > label {
position: absolute !important;
inset-inline-start: 0.75rem !important;
padding-inline: 0.35rem !important;
background-color: var(--color-white, #ffffff) !important;
font-weight: 400 !important;
color: #8c9097 !important;
z-index: 10 !important;
overflow: visible !important;
white-space: nowrap !important;
pointer-events: none !important;
}
/* Float label and highlight purple on focus or when date is selected */
:host ::ng-deep .date-picker-floating .floating-label--float > label,
:host ::ng-deep .date-picker-floating .floating-label:focus-within > label {
top: 0 !important;
transform: translateY(-50%) scale(0.85) !important;
transform-origin: left center !important;
font-weight: 400 !important;
color: var(--color-primary) !important;
}
/* Float label and highlight purple on focus or when date is selected */
.date-picker-floating .floating-label--float > label,
.date-picker-floating .floating-label:focus-within > label {
top: 0 !important;
transform: translateY(-50%) scale(0.85) !important;
transform-origin: left center !important;
font-weight: 400 !important;
color: var(--color-primary, #7c3deb) !important;
}
:host ::ng-deep .date-picker-floating .floating-label--float .form-control,
:host ::ng-deep .date-picker-floating .floating-label:focus-within .form-control {
color: var(--color-defaulttextcolor) !important;
border-color: var(--color-primary) !important;
}
/* Show native datetime format / selected date value when floated or focused */
:host ::ng-deep .date-picker-floating .floating-label--float .form-control::-webkit-datetime-edit,
:host ::ng-deep .date-picker-floating .floating-label:focus-within .form-control::-webkit-datetime-edit,
:host ::ng-deep .date-picker-floating .floating-label--float .form-control::-webkit-datetime-edit-fields-wrapper,
:host ::ng-deep .date-picker-floating .floating-label:focus-within .form-control::-webkit-datetime-edit-fields-wrapper,
:host ::ng-deep .date-picker-floating .floating-label--float .form-control::-webkit-datetime-edit-text,
:host ::ng-deep .date-picker-floating .floating-label:focus-within .form-control::-webkit-datetime-edit-text,
:host ::ng-deep .date-picker-floating .floating-label--float .form-control::-webkit-datetime-edit-month-field,
:host ::ng-deep .date-picker-floating .floating-label:focus-within .form-control::-webkit-datetime-edit-month-field,
:host ::ng-deep .date-picker-floating .floating-label--float .form-control::-webkit-datetime-edit-day-field,
:host ::ng-deep .date-picker-floating .floating-label:focus-within .form-control::-webkit-datetime-edit-day-field,
:host ::ng-deep .date-picker-floating .floating-label--float .form-control::-webkit-datetime-edit-year-field,
:host ::ng-deep .date-picker-floating .floating-label:focus-within .form-control::-webkit-datetime-edit-year-field {
color: var(--color-defaulttextcolor) !important;
}
:host-context(.dark) ::ng-deep .date-picker-floating .form-control,
:host-context(.dark) ::ng-deep .date-picker-floating .floating-label > label {
background-color: var(--color-bodybg) !important;
}
:host-context(.dark) ::ng-deep .date-picker-floating .floating-label--float .form-control,
:host-context(.dark) ::ng-deep .date-picker-floating .floating-label:focus-within .form-control {
color: var(--color-white) !important;
}
:host-context(.dark) ::ng-deep .date-picker-floating .floating-label--float .form-control::-webkit-datetime-edit,
:host-context(.dark) ::ng-deep .date-picker-floating .floating-label:focus-within .form-control::-webkit-datetime-edit {
color: var(--color-white) !important;
}
/* Calendar icon picker indicator styling */
:host ::ng-deep .form-control::-webkit-calendar-picker-indicator {
cursor: pointer;
opacity: 0.6;
transition: opacity 0.2s ease, filter 0.2s ease;
&:hover {
opacity: 1;
.date-picker-floating .floating-label--float .form-control,
.date-picker-floating .floating-label:focus-within .form-control {
color: var(--color-defaulttextcolor, #334155) !important;
border-color: var(--color-primary, #7c3deb) !important;
}
}
/* Invert calendar picker icon color to white in dark mode */
:host-context(.dark) ::ng-deep .form-control::-webkit-calendar-picker-indicator {
filter: invert(1) !important;
opacity: 0.8;
/* Dark mode styling replacing deprecated :host-context */
.dark,
[data-theme-mode="dark"] {
:host ::ng-deep {
.date-picker-floating .floating-label .form-control {
background-color: var(--color-bodybg, #1a1c1e) !important;
border-color: rgba(255, 255, 255, 0.12);
color: #e2e8f0 !important;
}
&:hover {
opacity: 1;
.date-picker-floating .floating-label > label {
background-color: var(--color-bodybg, #1a1c1e) !important;
color: #94a3b8 !important;
}
.date-picker-floating .floating-label--float > label,
.date-picker-floating .floating-label:focus-within > label {
color: #c084fc !important;
}
.date-picker-floating .floating-label--float .form-control,
.date-picker-floating .floating-label:focus-within .form-control {
color: #ffffff !important;
border-color: #a855f7 !important;
}
}
}
}
@@ -1,28 +1,52 @@
import {
AfterViewInit,
ChangeDetectionStrategy,
Component,
ElementRef,
Injector,
ViewChild,
computed,
effect,
forwardRef,
inject,
input,
signal,
viewChild,
} from '@angular/core';
import {
AbstractControl,
ControlValueAccessor,
FormsModule,
NG_VALUE_ACCESSOR,
NgControl,
ReactiveFormsModule,
} from '@angular/forms';
import { FormField } from '../form-field/form-field';
import { ValidationMessageMap } from '../form-validation-message/form-validation-message';
import {
SpkFlatpickr,
SpkFlatpickrMode,
formatSingleDate,
fromFlatpickrValue,
toFlatpickrValue,
} from '../../../../@spk/plugins&reusable/spk-flatpickr/spk-flatpickr';
export interface DateRangeValue {
from: string | null;
to: string | null;
}
export type DatePickerValue = string | DateRangeValue | null;
export function formatSingleDateValue(raw: any): string | null {
return formatSingleDate(raw);
}
@Component({
selector: 'app-form-date-picker',
standalone: true,
imports: [FormField],
imports: [FormsModule, ReactiveFormsModule, FormField, SpkFlatpickr],
templateUrl: './form-date-picker.html',
styleUrl: './form-date-picker.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
@@ -34,17 +58,25 @@ import { ValidationMessageMap } from '../form-validation-message/form-validation
},
],
})
export class FormDatePicker implements ControlValueAccessor {
export class FormDatePicker implements ControlValueAccessor, AfterViewInit {
private readonly injector = inject(Injector);
readonly flatpickrComponent = viewChild<SpkFlatpickr>('flatpickrComponent');
@ViewChild('wrapperDiv', { static: false }) wrapperDivRef?: ElementRef<HTMLElement>;
@ViewChild('calendarIcon', { static: false }) calendarIconRef?: ElementRef<HTMLElement>;
@ViewChild('clearBtn', { static: false }) clearBtnRef?: ElementRef<HTMLElement>;
readonly inputId = input.required<string>();
readonly label = input.required<string>();
readonly placeholder = input('');
readonly mode = input<SpkFlatpickrMode>('single');
readonly required = input(false);
readonly readonly = input(false);
readonly disabled = input(false);
readonly min = input<string | null>(null);
readonly max = input<string | null>(null);
readonly clearable = input(true);
readonly min = input<string | undefined>(undefined);
readonly max = input<string | undefined>(undefined);
readonly hint = input<string | null>(null);
readonly help = input<string | null>(null);
readonly description = input<string | null>(null);
@@ -57,12 +89,12 @@ export class FormDatePicker implements ControlValueAccessor {
readonly labelClass = input('');
readonly fieldContentClass = input('');
readonly value = signal<string | null>(null);
readonly value = signal<DatePickerValue>(null);
readonly formDisabled = signal(false);
readonly focused = signal(false);
private readonly controlStateVersion = signal(0);
private onChange: (value: string | null) => void = () => {};
private onChange: (value: DatePickerValue) => void = () => {};
private onTouched: () => void = () => {};
readonly control = computed<AbstractControl | null>(() =>
@@ -71,7 +103,16 @@ export class FormDatePicker implements ControlValueAccessor {
readonly isFloating = computed(() => this.variant() === 'floating');
readonly isDisabled = computed(() => this.disabled() || this.formDisabled());
readonly hasValue = computed(() => !!this.value());
readonly hasValue = computed(() => {
const val = this.value();
if (!val) return false;
if (typeof val === 'string') return !!val.trim();
if (typeof val === 'object' && val !== null && 'from' in val) {
return !!(val.from || val.to);
}
return false;
});
readonly shouldFloat = computed(() => this.focused() || this.hasValue());
readonly showError = computed(() => {
this.controlStateVersion();
@@ -84,6 +125,30 @@ export class FormDatePicker implements ControlValueAccessor {
this.showError() ? 'is-invalid' : '',
].filter(Boolean).join(' '));
readonly flatpickrValue = computed(() => {
return toFlatpickrValue(this.value(), this.mode());
});
readonly rangeFrom = computed<string | null>(() => {
const val = this.value();
if (this.mode() === 'range' && val && typeof val === 'object' && 'from' in val) {
return val.from ?? null;
}
return null;
});
readonly rangeTo = computed<string | null>(() => {
const val = this.value();
if (this.mode() === 'range' && val && typeof val === 'object' && 'to' in val) {
return val.to ?? null;
}
return null;
});
readonly isRangeSelected = computed<boolean>(() => {
return !!(this.rangeFrom() && this.rangeTo());
});
constructor() {
effect((onCleanup) => {
const control = this.control();
@@ -93,15 +158,15 @@ export class FormDatePicker implements ControlValueAccessor {
}
const statusSubscription = control.statusChanges.subscribe(() => {
this.controlStateVersion.update(value => value + 1);
this.controlStateVersion.update(val => val + 1);
});
const valueSubscription = control.valueChanges.subscribe(() => {
this.controlStateVersion.update(value => value + 1);
this.controlStateVersion.update(val => val + 1);
});
const eventsSubscription = control.events?.subscribe(() => {
this.controlStateVersion.update(value => value + 1);
this.controlStateVersion.update(val => val + 1);
});
onCleanup(() => {
@@ -112,11 +177,48 @@ export class FormDatePicker implements ControlValueAccessor {
});
}
writeValue(value: string | null): void {
this.value.set(value ?? null);
ngAfterViewInit(): void {
const fp = this.flatpickrComponent();
if (fp) {
if (this.wrapperDivRef?.nativeElement) {
fp.registerIgnoredElement(this.wrapperDivRef.nativeElement);
}
if (this.calendarIconRef?.nativeElement) {
fp.registerIgnoredElement(this.calendarIconRef.nativeElement);
}
if (this.clearBtnRef?.nativeElement) {
fp.registerIgnoredElement(this.clearBtnRef.nativeElement);
}
}
}
registerOnChange(fn: (value: string | null) => void): void {
writeValue(val: DatePickerValue): void {
if (!val) {
this.value.set(null);
return;
}
if (this.mode() === 'range') {
if (typeof val === 'object' && val !== null && ('from' in val || 'to' in val)) {
const range = val as DateRangeValue;
const from = formatSingleDate(range.from);
const to = formatSingleDate(range.to);
this.value.set(from || to ? { from, to } : null);
} else if (typeof val === 'string') {
const parts = val.split(/\s+to\s+|\s+-\s+/i);
const from = formatSingleDate(parts[0]);
const to = formatSingleDate(parts[1]);
this.value.set(from || to ? { from, to } : null);
} else {
this.value.set(null);
}
} else {
const normalized = formatSingleDate(val);
this.value.set(normalized);
}
}
registerOnChange(fn: (value: DatePickerValue) => void): void {
this.onChange = fn;
}
@@ -128,11 +230,50 @@ export class FormDatePicker implements ControlValueAccessor {
this.formDisabled.set(isDisabled);
}
onInput(event: Event): void {
const target = event.target as HTMLInputElement;
const nextValue = target.value.trim() || null;
this.value.set(nextValue);
this.onChange(nextValue);
openCalendar(event?: MouseEvent): void {
if (this.isDisabled() || this.readonly()) {
return;
}
const target = event?.target as HTMLElement | undefined;
if (target && (target.tagName === 'INPUT' || target.closest('.spk-date-picker-clear-btn'))) {
return;
}
if (event) {
event.preventDefault();
event.stopPropagation();
}
const fp = this.flatpickrComponent();
if (fp) {
fp.toggle();
}
}
clearDate(event: MouseEvent): void {
event.preventDefault();
event.stopPropagation();
if (this.isDisabled() || this.readonly()) {
return;
}
this.value.set(null);
this.onChange(null);
this.onTouched();
const fp = this.flatpickrComponent();
if (fp) {
fp.clear();
}
}
onDateChange(nextRawValue: any): void {
const normalized = fromFlatpickrValue(nextRawValue, this.mode());
this.value.set(normalized);
this.onChange(normalized);
this.onTouched();
}
onFocus(): void {
@@ -143,5 +284,4 @@ export class FormDatePicker implements ControlValueAccessor {
this.focused.set(false);
this.onTouched();
}
}
@@ -4,10 +4,10 @@
role="alert"
aria-live="polite"
>
<i
<!-- <i
class="ri-error-warning-line me-1 align-middle"
aria-hidden="true"
></i>
></i> -->
{{ message() }}
</p>
+4 -6
View File
@@ -146,12 +146,10 @@
<div class="modern-modal-footer">
@if (showCancelButton()) {
<app-button
action="cancel"
[label]="cancelLabel()"
[showIcon]="true"
[disabled]="loading()"
(buttonClicked)="close()" />
<app-button action="cancel" [variant]="'custom'" [label]="cancelLabel()" [showIcon]="true"
className="onboarding-nav-btn onboarding-save-btn ti-btn-outline-primary whitespace-nowrap justify-center"
(buttonClicked)="close()"></app-button>
}
@if (showSubmitButton()) {
+120
View File
@@ -460,6 +460,126 @@
background: #ffffff;
}
/* ==========================================================
Dark Mode
========================================================== */
:host-context(.dark) .modern-modal {
background: var(--color-bodybg) !important;
box-shadow:
0 15px 40px rgba(0, 0, 0, .25),
0 30px 80px rgba(0, 0, 0, .35);
}
:host-context(.dark) .modern-modal-body {
color: var(--color-defaulttextcolor) !important;
}
:host-context(.dark) .modern-modal-body::-webkit-scrollbar-thumb {
background: var(--color-inputborder) !important;
}
:host-context(.dark) .modern-modal-body::-webkit-scrollbar-thumb:hover {
background: var(--color-textmuted) !important;
}
:host-context(.dark) .modern-modal-footer {
background: var(--color-bodybg) !important;
border-top: 1px solid var(--color-inputborder) !important;
}
:host-context(.dark) ::ng-deep .modern-modal-footer app-button:first-child button {
background: var(--color-light) !important;
border-color: var(--color-inputborder) !important;
color: var(--color-defaulttextcolor) !important;
}
:host-context(.dark) ::ng-deep .modern-modal-footer app-button:first-child button:hover {
background: var(--color-bodybg) !important;
border-color: var(--color-textmuted) !important;
}
:host-context(.dark) ::ng-deep .modern-modal-footer app-button:last-child button {
box-shadow:
0 8px 20px rgba(125, 61, 235, 0.55) !important;
}
:host-context(.dark) ::ng-deep .modern-modal-footer app-button:last-child button:hover {
box-shadow:
0 12px 28px rgba(125, 61, 235, .65) !important;
}
/* ==========================================================
Modal form-control borders in dark mode
========================================================== */
:host-context(.dark) ::ng-deep .modern-modal {
.shared-form-input,
.custom-floating-input,
.form-control,
.form-select,
.ti-form-input,
.ti-form-select,
.ti-form-control,
.ng-select-container,
textarea,
input[type="text"],
input:not([type]),
input[type="email"],
input[type="password"],
input[type="number"],
input[type="tel"],
input[type="url"],
input[type="search"],
input[type="date"],
input[type="datetime-local"],
input[type="month"],
input[type="time"],
input[type="week"] {
border-color: rgb(115 122 130) !important;
}
.form-check-input:not(:checked) {
border-color: rgb(115 122 130) !important;
}
/* keep focus / validation colors */
.shared-form-control.is-focused .shared-form-input,
.shared-form-input:focus,
.custom-floating-input:focus,
.form-control:focus,
.form-select:focus,
.ti-form-input:focus,
.ti-form-select:focus,
.ti-form-control:focus,
.ng-select.ng-select-focused .ng-select-container,
textarea:focus {
border-color: var(--color-primary) !important;
}
.form-control.is-invalid,
.form-select.is-invalid,
.ti-form-input.is-invalid,
.ti-form-select.is-invalid,
.ti-form-control.is-invalid,
textarea.is-invalid,
.has-error .shared-form-input,
.ng-invalid.ng-touched,
.custom-floating-input.ng-invalid.ng-touched {
border-color: var(--color-danger) !important;
}
.form-check-input:checked,
.form-check-input.is-invalid {
border-color: var(--color-primary) !important;
}
.form-check-input.is-invalid:not(:checked) {
border-color: var(--color-danger) !important;
}
}
/* ==========================================================
Mobile
@@ -0,0 +1,8 @@
import { DataTableFilterActionsDirective } from './data-table-filter-actions.directive';
describe('DataTableFilterActionsDirective', () => {
it('should create an instance', () => {
const directive = new DataTableFilterActionsDirective();
expect(directive).toBeTruthy();
});
});
@@ -0,0 +1,18 @@
import {
Directive,
TemplateRef,
inject
} from '@angular/core';
@Directive({
selector: '[appDataTableFilterActions]',
standalone: true
})
export class DataTableFilterActionsDirective {
readonly templateRef = inject(
TemplateRef<unknown>
);
}