feat(web): campos de formulario SBO reutilizables
- sbo-field: fila etiqueta + control, con obligatorio (*), flecha dorada, etiqueta enlazable y mensaje de error. - sbo-text-field, sbo-textarea-field, sbo-number-field, sbo-date-field, sbo-select-field y sbo-lookup-field, con enlace bidireccional por señales ([(value)]) y entradas comunes (label, required, readonly, disabled, error). - sbo-number-field: formateado fuera de foco (1,234.50), acepta 12,5 y 1,234.50 al escribir, redondea a los decimales indicados y conserva el valor si el texto no es un número (parseDecimal). - sbo-select-field devuelve el valor con su tipo original (número o texto). - sbo-lookup-field: botón "…" y F4 abren sbo-value-help (VhLookup); elegir rellena el campo, emite la fila y devuelve el foco; escribir el código a mano sigue siendo válido. - Estilos globales de formulario (forms.css) tomados del mockup, con ancho de etiqueta configurable (--label-w). - Verificado en Chrome contra SAP real, en claro y oscuro: cliente elegido de la lista rellena código, nombre y RUC; edición de números; selects. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3d5a81e806
commit
aa2c483723
16 files changed
+917
-1
No files matched your search
@@ -0,0 +1,91 @@
|
||||
import { ChangeDetectionStrategy, Component, booleanAttribute, input, output } from '@angular/core';
|
||||
|
||||
/**
|
||||
* Fila de formulario SBO: etiqueta a la izquierda y el control proyectado a la derecha.
|
||||
* `action` pinta la flecha dorada de SAP (campo que enlaza a otro objeto o a una lista).
|
||||
*/
|
||||
@Component({
|
||||
selector: 'sbo-field',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<div class="f">
|
||||
<label
|
||||
[attr.for]="for()"
|
||||
[class.req]="required()"
|
||||
[class.ga]="action()"
|
||||
[class.link]="linkable()"
|
||||
(click)="labelClick.emit()"
|
||||
>{{ label() }}</label
|
||||
>
|
||||
<div class="ctl">
|
||||
<ng-content />
|
||||
@if (error()) {
|
||||
<span class="err" role="alert" [id]="for() + '-err'">{{ error() }}</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
styles: `
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
.f {
|
||||
display: grid;
|
||||
grid-template-columns: var(--label-w, 165px) minmax(0, 1fr);
|
||||
align-items: start;
|
||||
gap: 8px;
|
||||
min-height: 24px;
|
||||
}
|
||||
label {
|
||||
padding-top: 3px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
label.req::after {
|
||||
content: ' *';
|
||||
color: var(--err);
|
||||
}
|
||||
label.ga {
|
||||
position: relative;
|
||||
padding-left: 14px;
|
||||
}
|
||||
label.ga::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
border-left: 8px solid var(--gold);
|
||||
border-top: 5px solid transparent;
|
||||
border-bottom: 5px solid transparent;
|
||||
}
|
||||
label.link {
|
||||
cursor: pointer;
|
||||
}
|
||||
label.link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.ctl {
|
||||
min-width: 0;
|
||||
}
|
||||
.err {
|
||||
display: block;
|
||||
margin-top: 1px;
|
||||
font-size: 11.5px;
|
||||
color: var(--err);
|
||||
}
|
||||
`,
|
||||
})
|
||||
export class SboField {
|
||||
readonly label = input.required<string>();
|
||||
/** `id` del control, para enlazar la etiqueta. */
|
||||
readonly for = input<string>();
|
||||
readonly required = input(false, { transform: booleanAttribute });
|
||||
readonly action = input(false, { transform: booleanAttribute });
|
||||
/** La etiqueta es un enlace (ej. abrir el socio de negocio). */
|
||||
readonly linkable = input(false, { transform: booleanAttribute });
|
||||
readonly error = input<string>();
|
||||
|
||||
readonly labelClick = output<void>();
|
||||
}
|
||||
Reference in new issue
Block a user