refactor(web): filas de formulario con varios controles combinables

El mockup muestra que una fila es etiqueta + uno o varios controles en la
misma línea (Cliente = input + "...", Canal / Zona = dos mitades, Serie +
Número = 72 px + resto, select + número + botón) y que la cabecera tiene a
lo sumo 2 columnas. Los campos de un solo control no lo permitían.

- Controles sin etiqueta combinables dentro de sbo-field: sbo-input,
  sbo-textarea, sbo-number, sbo-date, sbo-select y el botón .sbo-btn;
  ocupan el espacio sobrante o un width fijo.
- El primer control de la fila toma el id de la etiqueta (claimId); los
  demás se nombran con ariaLabel. El error de la fila marca todos sus
  controles (aria-invalid / aria-describedby).
- Los campos con etiqueta (sbo-text-field, sbo-number-field, ...,
  sbo-lookup-field) pasan a ser atajos de un solo control con la misma API.
- CSS global: .sbo-group y .sbo-btn reemplazan a .sbo-pick/.sbo-pair.
- CLAUDE.md apunta al mockup renombrado (tools/oferta_venta.html).
- Verificado en Chrome contra SAP real, con una cabecera que reproduce las
  filas compuestas del mockup.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
GeorgeandClaude Sonnet 5.5 committed 2026-10-13 19:49:16 -05:00
1 parent aa2c483723
commit 68caf401a7
16 files changed
+531 -298

No files matched your search

+20 -6
View File
@@ -1,8 +1,13 @@
import { ChangeDetectionStrategy, Component, booleanAttribute, input, output } from '@angular/core';
let nextId = 0;
/**
* Fila de formulario SBO: etiqueta a la izquierda y el control proyectado a la derecha.
* Fila de formulario SBO: etiqueta a la izquierda y, a la derecha, uno o varios
* controles en una misma línea (input + botón, select + botón, serie + número...).
* `action` pinta la flecha dorada de SAP (campo que enlaza a otro objeto o a una lista).
*
* El primer control de la fila toma el `id` al que apunta la etiqueta (ver `claimId`).
*/
@Component({
selector: 'sbo-field',
@@ -10,7 +15,7 @@ import { ChangeDetectionStrategy, Component, booleanAttribute, input, output } f
template: `
<div class="f">
<label
[attr.for]="for()"
[attr.for]="controlId"
[class.req]="required()"
[class.ga]="action()"
[class.link]="linkable()"
@@ -18,9 +23,9 @@ import { ChangeDetectionStrategy, Component, booleanAttribute, input, output } f
>{{ label() }}</label
>
<div class="ctl">
<ng-content />
<div class="sbo-group"><ng-content /></div>
@if (error()) {
<span class="err" role="alert" [id]="for() + '-err'">{{ error() }}</span>
<span class="err" role="alert" [id]="errorId">{{ error() }}</span>
}
</div>
</div>
@@ -79,8 +84,6 @@ import { ChangeDetectionStrategy, Component, booleanAttribute, input, output } f
})
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). */
@@ -88,4 +91,15 @@ export class SboField {
readonly error = input<string>();
readonly labelClick = output<void>();
readonly controlId = `sbo-f-${++nextId}`;
readonly errorId = `${this.controlId}-err`;
private claimed = false;
/** El primer control de la fila recibe el id de la etiqueta; los demás deben llevar su `ariaLabel`. */
claimId(): string | null {
if (this.claimed) return null;
this.claimed = true;
return this.controlId;
}
}