Organization Onboarding Workflow
This commit is contained in:
@@ -8,6 +8,8 @@
|
||||
title() ||
|
||||
'Button'
|
||||
"
|
||||
[attr.aria-expanded]="ariaExpanded()"
|
||||
[attr.aria-controls]="ariaControls()"
|
||||
[attr.aria-busy]="loading() ? true : null"
|
||||
(click)="onClick($event)"
|
||||
>
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
: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;
|
||||
}
|
||||
|
||||
:host-context(.modern-modal-footer) button.ti-btn-light,
|
||||
:host-context(.onboarding-action-footer) button.ti-btn-light {
|
||||
border: 1px solid #dbe3ef;
|
||||
background: #ffffff;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
: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;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
:host-context(.modern-modal-footer) button.ti-btn-primary-full,
|
||||
:host-context(.modern-modal-footer) button.ti-btn-success-full,
|
||||
:host-context(.onboarding-action-footer) button.ti-btn-primary-full,
|
||||
:host-context(.onboarding-action-footer) button.ti-btn-success-full {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border: none;
|
||||
color: #ffffff;
|
||||
background: #7c3deb;
|
||||
box-shadow: 0 8px 20px rgba(125, 61, 235, 0.468);
|
||||
transition: .35s ease;
|
||||
}
|
||||
|
||||
:host-context(.modern-modal-footer) button.ti-btn-primary-full:hover:not(:disabled),
|
||||
:host-context(.modern-modal-footer) button.ti-btn-success-full:hover:not(:disabled),
|
||||
:host-context(.onboarding-action-footer) button.ti-btn-primary-full:hover:not(:disabled),
|
||||
:host-context(.onboarding-action-footer) button.ti-btn-success-full:hover:not(:disabled) {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 12px 28px rgba(37, 99, 235, .45);
|
||||
}
|
||||
|
||||
:host-context(.modern-modal-footer) button.ti-btn-primary-full::before,
|
||||
:host-context(.modern-modal-footer) button.ti-btn-success-full::before,
|
||||
:host-context(.onboarding-action-footer) button.ti-btn-primary-full::before,
|
||||
:host-context(.onboarding-action-footer) button.ti-btn-success-full::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -120%;
|
||||
width: 70%;
|
||||
height: 100%;
|
||||
background: linear-gradient(120deg, transparent, rgba(255, 255, 255, .55), transparent);
|
||||
transform: skewX(-25deg);
|
||||
animation: saveButtonShine 2s infinite;
|
||||
}
|
||||
|
||||
:host-context(.modern-modal-footer) button:disabled,
|
||||
:host-context(.onboarding-action-footer) button:disabled {
|
||||
opacity: .55;
|
||||
cursor: not-allowed;
|
||||
transform: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
:host-context(.onboarding-action-footer) button.onboarding-nav-btn {
|
||||
height: 34px;
|
||||
padding: 0 12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
:host-context(.onboarding-action-footer) button.onboarding-save-btn:hover:not(:disabled) {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 12px 28px rgba(37, 99, 235, .45);
|
||||
}
|
||||
|
||||
@keyframes saveButtonShine {
|
||||
0% {
|
||||
left: -120%;
|
||||
}
|
||||
|
||||
40% {
|
||||
left: 130%;
|
||||
}
|
||||
|
||||
100% {
|
||||
left: 130%;
|
||||
}
|
||||
}
|
||||
@@ -200,6 +200,7 @@ const BUTTON_ACTION_CONFIG: Record<ButtonAction, ButtonActionConfig> = {
|
||||
selector: 'app-button',
|
||||
standalone: true,
|
||||
templateUrl: './button.html',
|
||||
styleUrl: './button.scss',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class Button {
|
||||
@@ -231,6 +232,8 @@ export class Button {
|
||||
readonly iconClass = input('');
|
||||
|
||||
readonly ariaLabel = input<string | null>(null);
|
||||
readonly ariaExpanded = input<boolean | null>(null);
|
||||
readonly ariaControls = input<string | null>(null);
|
||||
readonly title = input<string | null>(null);
|
||||
|
||||
readonly buttonClicked = output<MouseEvent>();
|
||||
|
||||
@@ -145,7 +145,7 @@
|
||||
dark:hover:bg-white/10
|
||||
" [disabled]="isActionDisabled(action, row)" [class.opacity-50]="isActionDisabled(action, row)"
|
||||
[class.cursor-not-allowed]="isActionDisabled(action, row)"
|
||||
[class.!text-danger]="action.type === 'delete'" [attr.aria-label]="getActionLabel(action)"
|
||||
[class.!text-danger]="action.type === 'delete'"
|
||||
(click)="onDropdownActionClick($event, action, row)">
|
||||
@if (action.icon) {
|
||||
<i [class]="action.icon"></i>
|
||||
|
||||
@@ -517,6 +517,11 @@ export class DataTable<T extends DataTableRecord = DataTableRecord> {
|
||||
}
|
||||
|
||||
// 2. Automatic status detection
|
||||
|
||||
if (this.rowColorMode() === 'none') {
|
||||
return baseClass;
|
||||
}
|
||||
|
||||
const status =
|
||||
row['status'] ??
|
||||
row['state'] ??
|
||||
|
||||
@@ -77,6 +77,15 @@
|
||||
padding-block: 0.625rem 0.375rem;
|
||||
}
|
||||
|
||||
:host-context(app-filter-card) .autocomplete-control--floating {
|
||||
min-height: 2rem;
|
||||
}
|
||||
|
||||
:host-context(app-filter-card) .autocomplete-control--floating .autocomplete-input--floating {
|
||||
min-height: calc(2rem - 2px);
|
||||
padding-block: 0.5rem 0.25rem;
|
||||
}
|
||||
|
||||
.autocomplete-dropdown-indicator {
|
||||
position: absolute;
|
||||
inset-inline-end: 0.75rem;
|
||||
|
||||
@@ -135,6 +135,8 @@ export class Autocomplete<TItem, TValue> implements ControlValueAccessor {
|
||||
readonly formDisabled = signal(false);
|
||||
readonly panelWidth = signal(0);
|
||||
readonly focused = signal(false);
|
||||
readonly isForFilter = signal(false);
|
||||
|
||||
|
||||
readonly resolvedInputId = computed(() => this.inputId()?.trim() || this.generatedId);
|
||||
readonly panelId = computed(() => `${this.resolvedInputId()}-listbox`);
|
||||
@@ -170,6 +172,11 @@ export class Autocomplete<TItem, TValue> implements ControlValueAccessor {
|
||||
}
|
||||
return this.options().length ? '' : this.emptyText();
|
||||
});
|
||||
|
||||
// readonly resolveHeight = computed(() => {
|
||||
// return this.he() === 'sm' ? 'h-8' : 'h-10';
|
||||
// });
|
||||
|
||||
readonly resolvedInputClass = computed(() => {
|
||||
this.controlStateVersion();
|
||||
const control = this.control();
|
||||
|
||||
@@ -13,27 +13,29 @@
|
||||
[showValidationWhenDirty]="showValidationWhenDirty()"
|
||||
[submitAttempted]="submitAttempted()"
|
||||
[validationMessages]="validationMessages()"
|
||||
[wrapperClass]="wrapperClass()"
|
||||
[wrapperClass]="(isFloating() ? 'date-picker-floating ' : '') + wrapperClass()"
|
||||
[labelClass]="labelClass()"
|
||||
[contentClass]="fieldContentClass()"
|
||||
>
|
||||
<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()"
|
||||
/>
|
||||
<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()"
|
||||
/>
|
||||
</div>
|
||||
</app-form-field>
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
:host {
|
||||
display: block;
|
||||
width: 100%;
|
||||
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;
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
: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;
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
: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;
|
||||
}
|
||||
}
|
||||
|
||||
/* 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;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,6 +151,10 @@ export class FormField {
|
||||
|
||||
const value = control.value;
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return value.length > 0;
|
||||
}
|
||||
|
||||
return value !== null && value !== undefined && value !== '';
|
||||
});
|
||||
|
||||
|
||||
@@ -18,14 +18,14 @@
|
||||
.shared-form-input {
|
||||
display: block;
|
||||
width: 100%;
|
||||
min-height: 2.5rem;
|
||||
//min-height: 1.6rem;
|
||||
padding: 0.75rem 0.75rem 0.5rem;
|
||||
border: 1px solid var(--color-inputborder);
|
||||
border-radius: 0.5rem;
|
||||
background-color: var(--color-white);
|
||||
color: var(--color-defaulttextcolor);
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.25;
|
||||
line-height: 10px;
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
transition: border-color 0.2s ease;
|
||||
|
||||
@@ -1,225 +1,181 @@
|
||||
<app-form-field
|
||||
[label]="label()"
|
||||
[inputId]="resolvedInputId()"
|
||||
[control]="control()"
|
||||
[required]="required()"
|
||||
[disabled]="isDisabled()"
|
||||
[description]="description()"
|
||||
[hint]="hint()"
|
||||
[floating]="isFloating()"
|
||||
[forceFloat]="shouldFloat()"
|
||||
[labelPosition]="labelPosition()"
|
||||
[hideLabel]="hideLabel()"
|
||||
[hideValidation]="hideValidation()"
|
||||
[showValidationWhenDirty]="showValidationWhenDirty()"
|
||||
[submitAttempted]="submitAttempted()"
|
||||
[validationMessages]="validationMessages()"
|
||||
[wrapperClass]="(isFloating() ? 'form-select-floating ' : '') + wrapperClass()"
|
||||
[labelClass]="labelClass()"
|
||||
[contentClass]="fieldContentClass()"
|
||||
>
|
||||
<ng-select
|
||||
#select
|
||||
bindLabel="label"
|
||||
bindValue="value"
|
||||
appearance="outline"
|
||||
[items]="selectItems()"
|
||||
[ngModel]="modelValue()"
|
||||
[multiple]="isMultiple()"
|
||||
[searchable]="false"
|
||||
[editableSearchTerm]="resolvedEditableSearchTerm()"
|
||||
[clearable]="clearable()"
|
||||
[hideSelected]="hideSelected()"
|
||||
[closeOnSelect]="resolvedCloseOnSelect()"
|
||||
[maxSelectedItems]="maxSelectedItems()"
|
||||
[placeholder]="resolvedPlaceholder()"
|
||||
[loading]="loading()"
|
||||
[loadingText]="loadingText()"
|
||||
[notFoundText]="notFoundText()"
|
||||
[typeToSearchText]="typeToSearchText()"
|
||||
[clearAllText]="clearAllText()"
|
||||
[appendTo]="appendTo()"
|
||||
[dropdownPosition]="dropdownPosition()"
|
||||
[virtualScroll]="virtualScroll()"
|
||||
[bufferAmount]="bufferAmount()"
|
||||
[groupBy]="groupBy()"
|
||||
[selectableGroup]="selectableGroup()"
|
||||
[selectableGroupAsModel]="selectableGroupAsModel()"
|
||||
[selectOnTab]="selectOnTab()"
|
||||
[clearOnBackspace]="clearOnBackspace()"
|
||||
[readonly]="resolvedReadonly()"
|
||||
[labelForId]="resolvedInputId()"
|
||||
[inputAttrs]="inputAttrs()"
|
||||
[ariaLabel]="ariaLabel() || label() || resolvedPlaceholder()"
|
||||
[ngClass]="resolvedSelectClass()"
|
||||
(ngModelChange)="onValueChange($event)"
|
||||
(open)="onOpen()"
|
||||
(close)="onClose()"
|
||||
(focus)="onFocus()"
|
||||
(blur)="onBlur()"
|
||||
(clear)="onClear()"
|
||||
(scroll)="onScroll($event)"
|
||||
(scrollToEnd)="onScrollToEnd()"
|
||||
>
|
||||
@if (showDropdownHeader()) {
|
||||
<app-form-field [label]="label()" [inputId]="resolvedInputId()" [control]="control()" [required]="required()"
|
||||
[disabled]="isDisabled()" [description]="description()" [hint]="hint()" [floating]="isFloating()"
|
||||
[forceFloat]="shouldFloat()" [labelPosition]="labelPosition()" [hideLabel]="hideLabel()"
|
||||
[hideValidation]="hideValidation()" [showValidationWhenDirty]="showValidationWhenDirty()"
|
||||
[submitAttempted]="submitAttempted()" [validationMessages]="validationMessages()"
|
||||
[wrapperClass]="(isFloating() ? 'form-select-floating ' : '') + wrapperClass()" [labelClass]="labelClass()"
|
||||
[contentClass]="fieldContentClass()">
|
||||
<div class="relative w-full">
|
||||
<ng-select #select [bindLabel]="bindLabel()" [bindValue]="bindValue()" appearance="outline" [items]="selectItems()"
|
||||
[ngModel]="modelValue()" [multiple]="resolvedIsMultiple()" [searchable]="searchable()"
|
||||
[editableSearchTerm]="resolvedEditableSearchTerm()" [clearable]="clearable()" [hideSelected]="hideSelected()"
|
||||
[closeOnSelect]="resolvedCloseOnSelect()" [maxSelectedItems]="maxSelectedItems()"
|
||||
[placeholder]="isFloating() ? '' : resolvedPlaceholder()" [loading]="loading()" [loadingText]="loadingText()"
|
||||
[notFoundText]="notFoundText()" [typeToSearchText]="typeToSearchText()" [clearAllText]="clearAllText()"
|
||||
[appendTo]="appendTo()" [dropdownPosition]="dropdownPosition()" [virtualScroll]="virtualScroll()"
|
||||
[bufferAmount]="bufferAmount()" [groupBy]="groupBy()" [selectableGroup]="selectableGroup()"
|
||||
[selectableGroupAsModel]="selectableGroupAsModel()" [selectOnTab]="selectOnTab()"
|
||||
[clearOnBackspace]="clearOnBackspace()" [readonly]="resolvedReadonly()" [labelForId]="resolvedInputId()"
|
||||
[inputAttrs]="inputAttrs()" [ariaLabel]="ariaLabel() || label() || resolvedPlaceholder()"
|
||||
[ngClass]="resolvedSelectClass()" (ngModelChange)="onValueChange($event)" (open)="onOpen()" (close)="onClose()"
|
||||
(focus)="onFocus()" (blur)="onBlur()" (clear)="onClear()" (scroll)="onScroll($event)"
|
||||
(scrollToEnd)="onScrollToEnd()">
|
||||
@if (showDropdownHeader()) {
|
||||
<ng-template ng-header-tmp>
|
||||
@if (searchable()) {
|
||||
<div class="w-full">
|
||||
<input
|
||||
type="search"
|
||||
class="form-control form-control-sm w-full rounded-sm border-defaultborder text-defaulttextcolor dark:border-defaultborder/10 dark:text-white/70"
|
||||
[value]="searchTerm()"
|
||||
[placeholder]="resolvedSearchPlaceholder()"
|
||||
[disabled]="isDisabled()"
|
||||
autocomplete="off"
|
||||
(input)="onSearchInput($event)"
|
||||
(keydown)="$event.stopPropagation()"
|
||||
/>
|
||||
</div>
|
||||
<div class="relative p-2 border-b border-defaultborder dark:border-defaultborder/10">
|
||||
<i class="ri-search-line absolute inset-y-0 start-4 my-auto h-4 w-4 text-textmuted leading-none"></i>
|
||||
<input type="search"
|
||||
class="form-control form-control-sm !h-8 !min-h-0 w-full rounded-sm border border-defaultborder !ps-8 !pe-3 !py-1 text-xs text-defaulttextcolor placeholder:text-textmuted dark:border-defaultborder/20 dark:bg-bodybg dark:text-white/80 focus:border-primary"
|
||||
[value]="searchTerm()" [placeholder]="resolvedSearchPlaceholder()" [disabled]="isDisabled()"
|
||||
autocomplete="off" (input)="onSearchInput($event)" (keydown)="$event.stopPropagation()" />
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (isMultiple() && showSelectAll()) {
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full items-center gap-2 border-b border-defaultborder px-3 py-2 text-start text-[0.8125rem] text-defaulttextcolor hover:bg-light dark:border-defaultborder/10 dark:text-white/70 dark:hover:bg-black/20"
|
||||
[class.text-primary]="allSelected() || partiallySelected()"
|
||||
[disabled]="isDisabled()"
|
||||
(click)="toggleSelectAll()"
|
||||
>
|
||||
@if (showCheckboxes()) {
|
||||
<span
|
||||
class="flex size-4 shrink-0 items-center justify-center rounded-sm border border-defaultborder bg-white text-[0.625rem] dark:border-defaultborder/10 dark:bg-bodybg"
|
||||
[class.bg-primary]="allSelected() || partiallySelected()"
|
||||
[class.text-white]="allSelected() || partiallySelected()"
|
||||
aria-hidden="true"
|
||||
>
|
||||
@if (allSelected()) {
|
||||
<i class="ri-check-line leading-none"></i>
|
||||
} @else if (partiallySelected()) {
|
||||
<i class="ri-subtract-line leading-none"></i>
|
||||
}
|
||||
</span>
|
||||
}
|
||||
|
||||
<span class="min-w-0 flex-1 truncate">
|
||||
{{ selectAllLabel() }}
|
||||
</span>
|
||||
</button>
|
||||
}
|
||||
</ng-template>
|
||||
}
|
||||
|
||||
<ng-template ng-option-tmp let-item="item" let-item$="item$">
|
||||
<div [class]="getOptionContainerClass(item)">
|
||||
@if (isMultiple() && showCheckboxes()) {
|
||||
@if (resolvedIsMultiple() && showSelectAll()) {
|
||||
<button type="button"
|
||||
class="flex w-full items-center gap-2 border-b border-defaultborder px-3 py-2 text-start text-[0.8125rem] text-defaulttextcolor hover:bg-light dark:border-defaultborder/10 dark:text-white/70 dark:hover:bg-black/20"
|
||||
[class.text-primary]="allSelected() || partiallySelected()" [disabled]="isDisabled()"
|
||||
(click)="toggleSelectAll()">
|
||||
@if (showCheckboxes()) {
|
||||
<span
|
||||
class="mt-0.5 flex size-4 shrink-0 items-center justify-center rounded-sm border border-defaultborder bg-white text-[0.625rem] dark:border-defaultborder/10 dark:bg-bodybg"
|
||||
[class.bg-primary]="isOptionSelected(item)"
|
||||
[class.text-white]="isOptionSelected(item)"
|
||||
aria-hidden="true"
|
||||
>
|
||||
@if (isOptionSelected(item)) {
|
||||
<i class="ri-check-line leading-none"></i>
|
||||
class="flex size-4 shrink-0 items-center justify-center rounded-sm border text-[0.625rem] transition-colors"
|
||||
[class.border-primary]="allSelected() || partiallySelected()"
|
||||
[class.bg-primary]="allSelected() || partiallySelected()"
|
||||
[class.text-white]="allSelected() || partiallySelected()"
|
||||
[class.border-defaultborder]="!allSelected() && !partiallySelected()"
|
||||
[class.bg-white]="!allSelected() && !partiallySelected()"
|
||||
[class.dark:border-defaultborder/20]="!allSelected() && !partiallySelected()"
|
||||
[class.dark:bg-bodybg]="!allSelected() && !partiallySelected()" aria-hidden="true">
|
||||
@if (allSelected()) {
|
||||
<i class="ri-check-line leading-none font-bold"></i>
|
||||
} @else if (partiallySelected()) {
|
||||
<i class="ri-subtract-line leading-none font-bold"></i>
|
||||
}
|
||||
</span>
|
||||
}
|
||||
}
|
||||
|
||||
@if (item.prefixText) {
|
||||
<span class="min-w-0 flex-1 truncate font-medium">
|
||||
{{ selectAllLabel() }}
|
||||
</span>
|
||||
</button>
|
||||
}
|
||||
</ng-template>
|
||||
}
|
||||
|
||||
<ng-template ng-option-tmp let-item="item" let-item$="item$">
|
||||
<div [class]="getOptionContainerClass(item)">
|
||||
@if (resolvedIsMultiple() && showCheckboxes()) {
|
||||
<span
|
||||
class="flex size-4 shrink-0 items-center justify-center rounded-sm border text-[0.625rem] transition-colors"
|
||||
[class.border-primary]="isOptionSelected(item)" [class.bg-primary]="isOptionSelected(item)"
|
||||
[class.text-white]="isOptionSelected(item)" [class.border-defaultborder]="!isOptionSelected(item)"
|
||||
[class.bg-white]="!isOptionSelected(item)" [class.dark:border-defaultborder/20]="!isOptionSelected(item)"
|
||||
[class.dark:bg-bodybg]="!isOptionSelected(item)" aria-hidden="true">
|
||||
@if (isOptionSelected(item)) {
|
||||
<i class="ri-check-line leading-none font-bold"></i>
|
||||
}
|
||||
</span>
|
||||
}
|
||||
|
||||
@if (item.prefixText) {
|
||||
<span [class]="getOptionPrefixClass(item)">
|
||||
{{ item.prefixText }}
|
||||
</span>
|
||||
}
|
||||
}
|
||||
|
||||
<span class="min-w-0 flex-1">
|
||||
<span [class]="getOptionLabelClass(item)">
|
||||
{{ getOptionDisplayLabel(item, item$.label) }}
|
||||
</span>
|
||||
<span class="min-w-0 flex-1">
|
||||
<span [class]="getOptionLabelClass(item)">
|
||||
{{ getOptionDisplayLabel(item, item$.label) }}
|
||||
</span>
|
||||
|
||||
@if (item.description) {
|
||||
@if (item.description) {
|
||||
<span class="mt-0.5 block truncate text-[0.75rem] text-inherit opacity-80">
|
||||
{{ item.description }}
|
||||
</span>
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
</ng-template>
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<ng-template ng-label-tmp let-item="item" let-clear="clear" let-label="label">
|
||||
<span class="inline-flex min-w-0 items-center gap-1">
|
||||
@if (getSelectionPrefixText(item)) {
|
||||
<ng-template ng-label-tmp let-item="item" let-clear="clear" let-label="label">
|
||||
<span class="inline-flex min-w-0 items-center gap-1">
|
||||
@if (getSelectionPrefixText(item)) {
|
||||
<span class="shrink-0 text-primary">
|
||||
{{ getSelectionPrefixText(item) }}
|
||||
</span>
|
||||
}
|
||||
}
|
||||
|
||||
<span class="truncate">
|
||||
{{ getSelectionDisplayLabel(item, label) }}
|
||||
</span>
|
||||
<span class="truncate">
|
||||
{{ getSelectionDisplayLabel(item, label) }}
|
||||
</span>
|
||||
|
||||
@if (isMultiple() && clearable() && !resolvedReadonly()) {
|
||||
<button
|
||||
type="button"
|
||||
class="ms-1 inline-flex text-white/80 hover:text-white"
|
||||
aria-label="Remove selected option"
|
||||
(click)="clear(item)"
|
||||
>
|
||||
@if (resolvedIsMultiple() && clearable() && !resolvedReadonly()) {
|
||||
<button type="button" class="ms-1 inline-flex text-white/80 hover:text-white"
|
||||
aria-label="Remove selected option" (click)="clear(item)">
|
||||
<i class="ri-close-line leading-none"></i>
|
||||
</button>
|
||||
}
|
||||
</span>
|
||||
</ng-template>
|
||||
}
|
||||
</span>
|
||||
</ng-template>
|
||||
|
||||
@if (isMultiple()) {
|
||||
@if (resolvedIsMultiple()) {
|
||||
<ng-template ng-multi-label-tmp let-items="items" let-clear="clear">
|
||||
<div class="flex min-w-0 items-center gap-1">
|
||||
<div class="flex min-w-0 max-w-full items-center gap-1.5 overflow-hidden">
|
||||
@if (items.length > 0) {
|
||||
<span class="inline-flex max-w-full items-center gap-1 rounded-sm bg-primary px-2 py-0.5 text-[0.75rem] text-white">
|
||||
<span class="truncate">
|
||||
{{ getMultiLabelText(items) }}
|
||||
</span>
|
||||
|
||||
@if (clearable() && !resolvedReadonly()) {
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex text-white/80 hover:text-white"
|
||||
aria-label="Remove selected option"
|
||||
(click)="clear(items[0])"
|
||||
>
|
||||
<i class="ri-close-line leading-none"></i>
|
||||
</button>
|
||||
}
|
||||
<span
|
||||
class="inline-flex min-w-0 max-w-full items-center gap-1.5 rounded-sm bg-primary px-2 py-0.5 text-[0.75rem] text-white">
|
||||
<span class="min-w-0 truncate font-medium">
|
||||
{{ getFirstOptionLabel(items) }}
|
||||
</span>
|
||||
|
||||
@if (items.length > 1) {
|
||||
<span
|
||||
class="shrink-0 rounded-full bg-white/25 px-1.5 py-0.5 text-[0.6875rem] font-bold text-white shadow-xs">
|
||||
+{{ items.length - 1 }}
|
||||
</span>
|
||||
}
|
||||
|
||||
@if (clearable() && !resolvedReadonly()) {
|
||||
<button type="button" class="shrink-0 inline-flex text-white/80 hover:text-white ms-0.5"
|
||||
aria-label="Remove selected option" (click)="clear(items[0])">
|
||||
<i class="ri-close-line leading-none"></i>
|
||||
</button>
|
||||
}
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
</ng-template>
|
||||
}
|
||||
}
|
||||
|
||||
@if (isMultiple() && showMultiSelectFooter()) {
|
||||
@if (resolvedIsMultiple() && showMultiSelectFooter()) {
|
||||
<ng-template ng-footer-tmp>
|
||||
<div class="flex items-center justify-between gap-2 border-t border-defaultborder px-3 py-2 dark:border-defaultborder/10">
|
||||
<div
|
||||
class="flex items-center justify-between gap-2 border-t border-defaultborder px-3 py-2 dark:border-defaultborder/10">
|
||||
<span class="text-[0.75rem] text-textmuted">
|
||||
{{ pendingValue().length }} selected
|
||||
</span>
|
||||
|
||||
<span class="inline-flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="ti-btn ti-btn-light !px-3 !py-1.5 !text-[0.75rem]"
|
||||
(click)="cancelSelection(select)"
|
||||
>
|
||||
<button type="button" class="ti-btn ti-btn-light !px-3 !py-1.5 !text-[0.75rem]"
|
||||
(click)="cancelSelection(select)">
|
||||
{{ cancelLabel() }}
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="ti-btn ti-btn-primary-full !px-3 !py-1.5 !text-[0.75rem]"
|
||||
(click)="confirmSelection(select)"
|
||||
>
|
||||
<button type="button" class="ti-btn ti-btn-primary-full !px-3 !py-1.5 !text-[0.75rem]"
|
||||
(click)="confirmSelection(select)">
|
||||
{{ confirmLabel() }}
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</ng-template>
|
||||
}
|
||||
</ng-select>
|
||||
|
||||
@if (!loading()) {
|
||||
<span class="form-select-dropdown-indicator" [class.is-expanded]="dropdownOpen()" aria-hidden="true">
|
||||
<i class="ri-arrow-down-s-line"></i>
|
||||
</span>
|
||||
}
|
||||
</ng-select>
|
||||
</app-form-field>
|
||||
</div>
|
||||
</app-form-field>
|
||||
@@ -5,31 +5,170 @@
|
||||
}
|
||||
|
||||
:host ::ng-deep .form-select-floating .floating-label .ng-select-container {
|
||||
min-height: 2.5rem;
|
||||
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: var(--color-defaulttextcolor);
|
||||
font-size: 0.8125rem;
|
||||
box-shadow: none;
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
:host ::ng-deep .form-select-floating .floating-label > label {
|
||||
padding-inline: 0.25rem;
|
||||
:host ::ng-deep .form-select-floating .floating-label {
|
||||
position: relative !important;
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
:host ::ng-deep .form-select-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-size: 0.75rem !important;
|
||||
font-weight: 400 !important;
|
||||
color: #8c9097 !important;
|
||||
z-index: 9999 !important;
|
||||
overflow: visible !important;
|
||||
white-space: nowrap !important;
|
||||
pointer-events: none !important;
|
||||
}
|
||||
|
||||
:host ::ng-deep .form-select-floating .floating-label--float>label,
|
||||
:host ::ng-deep .form-select-floating .floating-label:focus-within>label,
|
||||
:host ::ng-deep .form-select-floating:has(.ng-select-has-value)>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;
|
||||
}
|
||||
|
||||
:host ::ng-deep .ng-select .ng-arrow-wrapper {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.form-select-dropdown-indicator {
|
||||
position: absolute;
|
||||
inset-inline-end: 0.75rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--color-textmuted, #8c9097);
|
||||
font-size: 0.875rem;
|
||||
line-height: 1;
|
||||
pointer-events: none;
|
||||
z-index: 9999 !important;
|
||||
transition: transform 0.2s ease, color 0.2s ease;
|
||||
|
||||
&.is-expanded {
|
||||
transform: translateY(-50%) rotate(180deg) !important;
|
||||
color: var(--color-primary) !important;
|
||||
}
|
||||
}
|
||||
|
||||
:host ::ng-deep .form-select-floating .floating-label .ng-select .ng-select-container .ng-value-container {
|
||||
padding-inline-start: 0.75rem;
|
||||
padding-inline-end: 2.25rem;
|
||||
padding-block: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:host ::ng-deep .form-select-floating .floating-label--float .ng-select .ng-select-container .ng-value-container,
|
||||
:host ::ng-deep .form-select-floating .floating-label:focus-within .ng-select .ng-select-container .ng-value-container {
|
||||
padding-top: 0.5rem !important;
|
||||
padding-bottom: 0.15rem !important;
|
||||
}
|
||||
|
||||
:host ::ng-deep .form-select-floating .floating-label .ng-select.ng-select-multiple .ng-select-container .ng-value-container {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
gap: 0.375rem;
|
||||
align-items: center;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:host ::ng-deep .form-select-floating .floating-label .ng-select.ng-select-multiple .ng-select-container .ng-value-container .ng-value {
|
||||
margin: 0;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:host ::ng-deep .form-select-floating .floating-label .ng-select.ng-select-multiple .ng-select-container .ng-value-container .ng-input {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
:host ::ng-deep .form-select-floating .floating-label .ng-select.ng-select-multiple .ng-select-container .ng-value-container .ng-placeholder {
|
||||
top: auto;
|
||||
}
|
||||
|
||||
:host ::ng-deep .form-select-floating .floating-label--float .ng-select-container,
|
||||
:host ::ng-deep .form-select-floating .floating-label:focus-within .ng-select-container {
|
||||
border-color: var(--color-primary);
|
||||
:host ::ng-deep .form-select-floating .floating-label:focus-within .ng-select-container,
|
||||
:host ::ng-deep .form-select-floating .ng-select.ng-select-has-value .ng-select-container,
|
||||
:host ::ng-deep .form-select-floating .ng-select.ng-select-opened .ng-select-container,
|
||||
:host ::ng-deep .form-select-floating .ng-select.ng-select-focused .ng-select-container {
|
||||
border-color: var(--color-primary) !important;
|
||||
}
|
||||
|
||||
:host ::ng-deep .form-select-floating .floating-label--invalid .ng-select-container {
|
||||
border-color: var(--color-danger);
|
||||
border-color: var(--color-danger) !important;
|
||||
}
|
||||
|
||||
:host-context(.dark) ::ng-deep .form-select-floating .ng-select-container,
|
||||
:host-context(.dark) ::ng-deep .form-select-floating .floating-label > label {
|
||||
:host-context(.dark) ::ng-deep .form-select-floating .floating-label>label {
|
||||
background-color: var(--color-bodybg) !important;
|
||||
}
|
||||
|
||||
:host ::ng-deep .ng-select .ng-dropdown-panel {
|
||||
border-radius: 0.5rem !important;
|
||||
border: 1px solid var(--color-defaultborder, #e9edf6) !important;
|
||||
background-color: var(--color-white, #ffffff) !important;
|
||||
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.05) !important;
|
||||
overflow: hidden !important;
|
||||
margin-top: 4px !important;
|
||||
}
|
||||
|
||||
:host-context(.dark) ::ng-deep .ng-select .ng-dropdown-panel {
|
||||
border-color: rgba(255, 255, 255, 0.1) !important;
|
||||
background-color: var(--color-bodybg, #1a1e25) !important;
|
||||
}
|
||||
|
||||
:host ::ng-deep .ng-select .ng-dropdown-panel .ng-dropdown-header {
|
||||
border-bottom: none !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
:host ::ng-deep .ng-select .ng-dropdown-panel .ng-header-tmp button,
|
||||
:host ::ng-deep .ng-select .ng-dropdown-panel .ng-dropdown-panel-items .ng-option {
|
||||
padding: 0.5rem 0.75rem !important;
|
||||
}
|
||||
|
||||
:host ::ng-deep .ng-select.ng-select-multiple .ng-dropdown-panel .ng-dropdown-panel-items .ng-option.ng-option-selected:not(.ng-option-marked) {
|
||||
background-color: transparent !important;
|
||||
color: var(--color-defaulttextcolor) !important;
|
||||
}
|
||||
|
||||
:host-context(.dark) ::ng-deep .ng-select.ng-select-multiple .ng-dropdown-panel .ng-dropdown-panel-items .ng-option.ng-option-selected:not(.ng-option-marked) {
|
||||
color: var(--color-white) !important;
|
||||
}
|
||||
|
||||
:host ::ng-deep .ng-select .ng-dropdown-panel .ng-dropdown-panel-items .ng-option.ng-option-marked {
|
||||
background-color: var(--color-primary) !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
:host ::ng-deep .ng-select .ng-dropdown-panel .ng-dropdown-panel-items .ng-option.ng-option-marked * {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
Component,
|
||||
Injector,
|
||||
computed,
|
||||
effect,
|
||||
forwardRef,
|
||||
inject,
|
||||
input,
|
||||
@@ -64,6 +65,7 @@ export class FormSelect<TValue extends FormSelectPrimitive = string> implements
|
||||
private readonly generatedInputId = `form-select-${FormSelect.nextId++}`;
|
||||
|
||||
readonly inputId = input<string | null>(null);
|
||||
readonly id = input<string | null>(null);
|
||||
|
||||
readonly label = input('');
|
||||
readonly variant = input<'default' | 'floating'>('default');
|
||||
@@ -71,13 +73,19 @@ export class FormSelect<TValue extends FormSelectPrimitive = string> implements
|
||||
readonly name = input<string | null>(null);
|
||||
|
||||
readonly options = input<readonly FormSelectOption<TValue>[]>([]);
|
||||
readonly bindLabel = input('label');
|
||||
readonly bindValue = input('value');
|
||||
|
||||
readonly mode = input<FormSelectMode>('single');
|
||||
readonly multiple = input<boolean | null>(null);
|
||||
readonly defaultValue = input<FormSelectValue<TValue> | null>(null);
|
||||
|
||||
readonly searchable = input(true);
|
||||
|
||||
readonly searchMode = input<FormSelectSearchMode>('client');
|
||||
|
||||
readonly searchPlaceholder = input<string | null>(null);
|
||||
|
||||
readonly editableSearchTerm = input<boolean | null>(null);
|
||||
|
||||
readonly clearable = input(true);
|
||||
@@ -159,6 +167,7 @@ export class FormSelect<TValue extends FormSelectPrimitive = string> implements
|
||||
readonly fieldContentClass = input('');
|
||||
|
||||
readonly selectClass = input('');
|
||||
readonly className = input('');
|
||||
|
||||
readonly ariaLabel = input<string | null>(null);
|
||||
|
||||
@@ -193,9 +202,9 @@ export class FormSelect<TValue extends FormSelectPrimitive = string> implements
|
||||
readonly searchTerm = signal('');
|
||||
readonly hasFocus = signal(false);
|
||||
|
||||
private onChange: (value: FormSelectValue<TValue>) => void = () => {};
|
||||
private onChange: (value: FormSelectValue<TValue>) => void = () => { };
|
||||
|
||||
private onTouched: () => void = () => {};
|
||||
private onTouched: () => void = () => { };
|
||||
|
||||
readonly control = computed<AbstractControl | null>(() => {
|
||||
return this.injector.get(NgControl, null, {
|
||||
@@ -205,7 +214,7 @@ export class FormSelect<TValue extends FormSelectPrimitive = string> implements
|
||||
});
|
||||
|
||||
readonly resolvedInputId = computed(() =>
|
||||
this.inputId()?.trim() || this.generatedInputId
|
||||
this.inputId()?.trim() || this.id()?.trim() || this.generatedInputId
|
||||
);
|
||||
|
||||
readonly resolvedName = computed(() =>
|
||||
@@ -214,6 +223,10 @@ export class FormSelect<TValue extends FormSelectPrimitive = string> implements
|
||||
|
||||
readonly isMultiple = computed(() => this.mode() === 'multiple');
|
||||
|
||||
readonly resolvedIsMultiple = computed(() =>
|
||||
this.multiple() ?? this.mode() === 'multiple'
|
||||
);
|
||||
|
||||
readonly isDisabled = computed(() =>
|
||||
this.disabled() ||
|
||||
this.formDisabled() ||
|
||||
@@ -235,7 +248,7 @@ export class FormSelect<TValue extends FormSelectPrimitive = string> implements
|
||||
);
|
||||
|
||||
readonly resolvedCloseOnSelect = computed(() =>
|
||||
this.closeOnSelect() ?? !this.isMultiple()
|
||||
this.closeOnSelect() ?? !this.resolvedIsMultiple()
|
||||
);
|
||||
|
||||
readonly resolvedEditableSearchTerm = computed(() =>
|
||||
@@ -276,7 +289,7 @@ export class FormSelect<TValue extends FormSelectPrimitive = string> implements
|
||||
readonly showDropdownHeader = computed(() =>
|
||||
this.searchable() ||
|
||||
(
|
||||
this.isMultiple() &&
|
||||
this.resolvedIsMultiple() &&
|
||||
this.showSelectAll()
|
||||
)
|
||||
);
|
||||
@@ -291,7 +304,7 @@ export class FormSelect<TValue extends FormSelectPrimitive = string> implements
|
||||
|
||||
readonly modelValue = computed<FormSelectValue<TValue>>(() => {
|
||||
if (
|
||||
this.isMultiple() &&
|
||||
this.resolvedIsMultiple() &&
|
||||
this.showMultiSelectFooter() &&
|
||||
this.dropdownOpen()
|
||||
) {
|
||||
@@ -353,7 +366,7 @@ export class FormSelect<TValue extends FormSelectPrimitive = string> implements
|
||||
);
|
||||
|
||||
readonly resolvedSearchPlaceholder = computed(() =>
|
||||
this.placeholder().trim() ||
|
||||
this.searchPlaceholder()?.trim() ||
|
||||
(
|
||||
this.label().trim()
|
||||
? `Search ${this.label().trim()}`
|
||||
@@ -361,7 +374,7 @@ export class FormSelect<TValue extends FormSelectPrimitive = string> implements
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
|
||||
|
||||
readonly describedBy = computed(() => {
|
||||
const ids: string[] = [];
|
||||
@@ -437,12 +450,25 @@ export class FormSelect<TValue extends FormSelectPrimitive = string> implements
|
||||
'app-form-select-control',
|
||||
showInvalidState ? 'is-invalid' : '',
|
||||
this.isDisabled() ? 'opacity-60 pointer-events-none' : '',
|
||||
this.selectClass()
|
||||
this.selectClass(),
|
||||
this.className()
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
});
|
||||
|
||||
constructor() {
|
||||
effect(() => {
|
||||
const defaultValue = this.defaultValue();
|
||||
|
||||
if (this.control() || defaultValue === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.value.set(this.normalizeValue(defaultValue));
|
||||
});
|
||||
}
|
||||
|
||||
writeValue(value: FormSelectValue<TValue>): void {
|
||||
this.value.set(this.normalizeValue(value));
|
||||
}
|
||||
@@ -463,7 +489,7 @@ export class FormSelect<TValue extends FormSelectPrimitive = string> implements
|
||||
const normalizedValue = this.normalizeValue(incomingValue);
|
||||
|
||||
if (
|
||||
this.isMultiple() &&
|
||||
this.resolvedIsMultiple() &&
|
||||
this.showMultiSelectFooter() &&
|
||||
this.dropdownOpen()
|
||||
) {
|
||||
@@ -492,7 +518,7 @@ export class FormSelect<TValue extends FormSelectPrimitive = string> implements
|
||||
}
|
||||
|
||||
toggleSelectAll(): void {
|
||||
if (!this.isMultiple() || this.isDisabled()) {
|
||||
if (!this.resolvedIsMultiple() || this.isDisabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -516,7 +542,7 @@ export class FormSelect<TValue extends FormSelectPrimitive = string> implements
|
||||
this.searchTerm.set('');
|
||||
|
||||
if (
|
||||
this.isMultiple() &&
|
||||
this.resolvedIsMultiple() &&
|
||||
this.showMultiSelectFooter()
|
||||
) {
|
||||
this.pendingValue.set([
|
||||
@@ -559,7 +585,7 @@ export class FormSelect<TValue extends FormSelectPrimitive = string> implements
|
||||
}
|
||||
|
||||
confirmSelection(select: NgSelectComponent): void {
|
||||
if (!this.isMultiple()) {
|
||||
if (!this.resolvedIsMultiple()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -594,15 +620,18 @@ export class FormSelect<TValue extends FormSelectPrimitive = string> implements
|
||||
: firstLabel;
|
||||
}
|
||||
|
||||
getFirstOptionLabel(items: readonly FormSelectOption<TValue>[]): string {
|
||||
return items[0]?.label ?? '';
|
||||
}
|
||||
|
||||
trackOption(option: FormSelectOption<TValue>): TValue {
|
||||
return option.value;
|
||||
}
|
||||
|
||||
getOptionContainerClass(option: FormSelectOption<TValue>): string {
|
||||
return [
|
||||
'flex min-w-0 w-full items-start gap-2 text-inherit',
|
||||
option.disabled ? 'text-textmuted opacity-60' : '',
|
||||
this.isOptionSelected(option) && !option.disabled ? 'text-white' : ''
|
||||
'flex min-w-0 w-full items-center gap-2 text-inherit',
|
||||
option.disabled ? 'text-textmuted opacity-60' : ''
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
@@ -660,7 +689,7 @@ export class FormSelect<TValue extends FormSelectPrimitive = string> implements
|
||||
|
||||
private activeSelectedValues(): readonly TValue[] {
|
||||
if (
|
||||
this.isMultiple() &&
|
||||
this.resolvedIsMultiple() &&
|
||||
this.showMultiSelectFooter() &&
|
||||
this.dropdownOpen()
|
||||
) {
|
||||
@@ -679,7 +708,7 @@ export class FormSelect<TValue extends FormSelectPrimitive = string> implements
|
||||
}
|
||||
|
||||
private normalizeValue(value: FormSelectValue<TValue>): FormSelectValue<TValue> {
|
||||
if (this.isMultiple()) {
|
||||
if (this.resolvedIsMultiple()) {
|
||||
return this.isValueArray(value)
|
||||
? value.filter(item => !this.isEmptyStringValue(item))
|
||||
: [];
|
||||
|
||||
@@ -461,208 +461,6 @@
|
||||
}
|
||||
|
||||
|
||||
/* ============================
|
||||
Cancel Button
|
||||
============================ */
|
||||
|
||||
|
||||
::ng-deep .modern-modal-footer app-button:first-child button {
|
||||
|
||||
|
||||
height: 36px;
|
||||
|
||||
padding: 0 16px;
|
||||
|
||||
border-radius: 10px;
|
||||
|
||||
border: 1px solid #dbe3ef;
|
||||
|
||||
background: #ffffff;
|
||||
|
||||
color: #475569;
|
||||
|
||||
font-weight: 600;
|
||||
|
||||
font-size: 13px;
|
||||
|
||||
transition: .3s ease;
|
||||
|
||||
}
|
||||
|
||||
|
||||
::ng-deep .modern-modal-footer app-button:first-child button:hover {
|
||||
|
||||
|
||||
background: #f8fafc;
|
||||
|
||||
border-color: #cbd5e1;
|
||||
|
||||
transform: translateY(-2px);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* ============================
|
||||
Save Button
|
||||
============================ */
|
||||
|
||||
|
||||
::ng-deep .modern-modal-footer app-button:last-child button {
|
||||
|
||||
|
||||
position: relative;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
height: 36px;
|
||||
|
||||
padding: 0 16px;
|
||||
|
||||
border-radius: 10px;
|
||||
|
||||
border: none;
|
||||
|
||||
|
||||
color: #ffffff;
|
||||
|
||||
|
||||
// background:
|
||||
|
||||
// linear-gradient(135deg,
|
||||
// #111C43,
|
||||
// #2563eb);
|
||||
background: #7c3deb;
|
||||
|
||||
box-shadow:
|
||||
|
||||
0 8px 20px rgba(125, 61, 235, 0.468);
|
||||
|
||||
|
||||
font-weight: 600;
|
||||
|
||||
font-size: 13px;
|
||||
|
||||
|
||||
transition: .35s ease;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
::ng-deep .modern-modal-footer app-button:last-child button:hover {
|
||||
|
||||
|
||||
transform: translateY(-3px);
|
||||
|
||||
|
||||
box-shadow:
|
||||
|
||||
0 12px 28px rgba(37, 99, 235, .45);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* ============================
|
||||
Shine Animation
|
||||
============================ */
|
||||
|
||||
|
||||
::ng-deep .modern-modal-footer app-button:last-child button::before {
|
||||
|
||||
|
||||
content: "";
|
||||
|
||||
|
||||
position: absolute;
|
||||
|
||||
|
||||
top: 0;
|
||||
|
||||
left: -120%;
|
||||
|
||||
|
||||
width: 70%;
|
||||
|
||||
|
||||
height: 100%;
|
||||
|
||||
|
||||
background:
|
||||
|
||||
linear-gradient(120deg,
|
||||
transparent,
|
||||
rgba(255, 255, 255, .55),
|
||||
transparent);
|
||||
|
||||
|
||||
transform: skewX(-25deg);
|
||||
|
||||
|
||||
animation: saveButtonShine 2s infinite;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@keyframes saveButtonShine {
|
||||
|
||||
|
||||
0% {
|
||||
|
||||
left: -120%;
|
||||
|
||||
}
|
||||
|
||||
|
||||
40% {
|
||||
|
||||
left: 130%;
|
||||
|
||||
}
|
||||
|
||||
|
||||
100% {
|
||||
|
||||
left: 130%;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/* Disabled */
|
||||
|
||||
::ng-deep .modern-modal-footer button:disabled {
|
||||
|
||||
|
||||
opacity: .55;
|
||||
|
||||
cursor: not-allowed;
|
||||
|
||||
transform: none !important;
|
||||
|
||||
box-shadow: none !important;
|
||||
|
||||
}
|
||||
|
||||
::ng-deep .modern-modal-footer app-button button {
|
||||
|
||||
height: 36px;
|
||||
|
||||
padding: 0 16px;
|
||||
|
||||
border-radius: 10px;
|
||||
|
||||
font-size: 13px;
|
||||
|
||||
font-weight: 600;
|
||||
|
||||
transition: .3s ease;
|
||||
|
||||
}
|
||||
|
||||
/* ==========================================================
|
||||
Mobile
|
||||
========================================================== */
|
||||
|
||||
Reference in New Issue
Block a user