feat(design-system): add reusable Input primitive
ControlValueAccessor-based, standalone, OnPush. Sizes sm/md/lg, states default/error/success. Colors/radius/spacing/transitions consumed via existing tenant CSS vars (--border-color, --primary-color, --error-color, --success-color, etc) with fallbacks - compatible with bootstrap JSON theming and Project Editor live preview.
This commit is contained in:
16
src/app/shared/ui/input/input.component.html
Normal file
16
src/app/shared/ui/input/input.component.html
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<input
|
||||||
|
class="app-input"
|
||||||
|
[class.app-input--sm]="size() === 'sm'"
|
||||||
|
[class.app-input--md]="size() === 'md'"
|
||||||
|
[class.app-input--lg]="size() === 'lg'"
|
||||||
|
[class.app-input--error]="state() === 'error'"
|
||||||
|
[class.app-input--success]="state() === 'success'"
|
||||||
|
[type]="type()"
|
||||||
|
[placeholder]="placeholder()"
|
||||||
|
[disabled]="isDisabled || disabled()"
|
||||||
|
[attr.aria-label]="ariaLabel()"
|
||||||
|
[attr.aria-invalid]="state() === 'error' || null"
|
||||||
|
[value]="value"
|
||||||
|
(input)="handleInput($event)"
|
||||||
|
(blur)="handleBlur()"
|
||||||
|
/>
|
||||||
81
src/app/shared/ui/input/input.component.scss
Normal file
81
src/app/shared/ui/input/input.component.scss
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
:host {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host.app-input-host--full-width {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-input {
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid var(--border-color, #e4e4e7);
|
||||||
|
border-radius: var(--radius-md, 6px);
|
||||||
|
background: var(--bg-primary, #fff);
|
||||||
|
color: var(--text-primary, #1f322d);
|
||||||
|
font-family: inherit;
|
||||||
|
line-height: 1;
|
||||||
|
transition: border-color var(--transition-fast, 120ms ease),
|
||||||
|
box-shadow var(--transition-fast, 120ms ease);
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: var(--text-secondary, #6b7280);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
border-color: var(--primary-color, #2f6e5d);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--primary-color, #2f6e5d);
|
||||||
|
box-shadow: 0 0 0 2px color-mix(in srgb, var(--primary-color, #2f6e5d) 25%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-input--sm {
|
||||||
|
height: 32px;
|
||||||
|
padding: 0 var(--space-sm, 0.5rem);
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-input--md {
|
||||||
|
height: 40px;
|
||||||
|
padding: 0 var(--space-md, 1rem);
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-input--lg {
|
||||||
|
height: 48px;
|
||||||
|
padding: 0 var(--space-lg, 1.5rem);
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-input--error {
|
||||||
|
border-color: var(--error-color, #c0392b);
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
border-color: var(--error-color, #c0392b);
|
||||||
|
box-shadow: 0 0 0 2px color-mix(in srgb, var(--error-color, #c0392b) 25%, transparent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-input--success {
|
||||||
|
border-color: var(--success-color, #2e7d32);
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
border-color: var(--success-color, #2e7d32);
|
||||||
|
box-shadow: 0 0 0 2px color-mix(in srgb, var(--success-color, #2e7d32) 25%, transparent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.app-input {
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
64
src/app/shared/ui/input/input.component.ts
Normal file
64
src/app/shared/ui/input/input.component.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import { ChangeDetectionStrategy, Component, forwardRef, input } from '@angular/core';
|
||||||
|
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||||
|
|
||||||
|
export type InputSize = 'sm' | 'md' | 'lg';
|
||||||
|
export type InputState = 'default' | 'error' | 'success';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-input',
|
||||||
|
standalone: true,
|
||||||
|
templateUrl: './input.component.html',
|
||||||
|
styleUrl: './input.component.scss',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: NG_VALUE_ACCESSOR,
|
||||||
|
useExisting: forwardRef(() => InputComponent),
|
||||||
|
multi: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
host: {
|
||||||
|
'[class.app-input-host--full-width]': 'fullWidth()'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
export class InputComponent implements ControlValueAccessor {
|
||||||
|
readonly type = input<'text' | 'email' | 'password' | 'number' | 'search' | 'tel' | 'url'>('text');
|
||||||
|
readonly size = input<InputSize>('md');
|
||||||
|
readonly state = input<InputState>('default');
|
||||||
|
readonly placeholder = input('');
|
||||||
|
readonly disabled = input(false);
|
||||||
|
readonly fullWidth = input(false);
|
||||||
|
readonly ariaLabel = input<string | null>(null);
|
||||||
|
|
||||||
|
protected value: string | number = '';
|
||||||
|
protected isDisabled = false;
|
||||||
|
|
||||||
|
private onChange: (value: string | number) => void = () => {};
|
||||||
|
private onTouched: () => void = () => {};
|
||||||
|
|
||||||
|
writeValue(value: string | number): void {
|
||||||
|
this.value = value ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
registerOnChange(fn: (value: string | number) => void): void {
|
||||||
|
this.onChange = fn;
|
||||||
|
}
|
||||||
|
|
||||||
|
registerOnTouched(fn: () => void): void {
|
||||||
|
this.onTouched = fn;
|
||||||
|
}
|
||||||
|
|
||||||
|
setDisabledState(isDisabled: boolean): void {
|
||||||
|
this.isDisabled = isDisabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected handleInput(event: Event): void {
|
||||||
|
const target = event.target as HTMLInputElement;
|
||||||
|
this.value = target.value;
|
||||||
|
this.onChange(this.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected handleBlur(): void {
|
||||||
|
this.onTouched();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user