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:
@@ -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>
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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
|
||||
|
||||
+8
@@ -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();
|
||||
});
|
||||
});
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
import {
|
||||
Directive,
|
||||
TemplateRef,
|
||||
inject
|
||||
} from '@angular/core';
|
||||
|
||||
@Directive({
|
||||
selector: '[appDataTableFilterActions]',
|
||||
standalone: true
|
||||
})
|
||||
export class DataTableFilterActionsDirective {
|
||||
|
||||
readonly templateRef = inject(
|
||||
TemplateRef<unknown>
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user