refactor(sema): share spin field perceptual cascade

astra
dev 2 weeks ago
parent b052b238f3
commit 2dbf471b3f

@ -217,6 +217,12 @@ por su Morfo y el resto de capas separadas.
La [revisión de ToggleGroup](./sema-toggle-group.md) reutiliza ese prefijo con
target `item` y conserva su tercera regla local de rechazo; son 12 líneas netas
menos de producción, sin cambiar la cascada resultante.
La [revisión de manipulación directa](./sema-direct-manipulation.md) deja Slider,
ColorPicker y GradientBuilder sin extracción: ColorPicker ya falla `A-3.7` en su
Morfo y la demo congelada avisa `D-1.2`; el contrato requiere una decisión aparte.
[CssField y NumberField](./sema-spin-fields.md) sí comparten ahora tres reglas
idénticas, mientras la advertencia de CssField sigue local (cinco líneas netas
menos de producción).
## Eidos

@ -0,0 +1,59 @@
---
title: UIX — auditoría Sema de manipulación directa
type: notes
audience: human + agent
authority: process — evaluación, no contrato de componente
status: reviewed
---
# Sema de Slider, ColorPicker y GradientBuilder
Fecha: 2026-09-23. Se releyeron las cuatro guías de componente exigidas por
`AGENTS.md`, los README de ColorPicker y GradientBuilder, sus Morfos y la doctrina
de packs de `docs/architecture/sema.md`. El usuario autoriza deduplicación interna
que preserve el comportamiento; `web/` está excluido.
## Pre-flight
| Aspecto | Slider | ColorPicker | GradientBuilder | Decisión |
| ---------------------- | ---------------------------------------- | ------------------------------ | ---------------------------- | -------------------------------------------------- |
| Superficie de arrastre | Provider | Area | Track | Mantener el selector tipado y el target propio |
| `handle-pick` | `air`, tap `tick` 0.22/6, sound + haptic | Idéntico | Idéntico | Compartir sólo la regla Sema |
| `handle-drag` | Sound + haptic; payload de emisión | Idéntico | Idéntico | No añadir `sound` que pise el payload dinámico |
| `commit-set` | Sin regla adicional en este pack | `settle`, tick 0.18/8 | Idéntico | Compartir la firma, conservar el selector Provider |
| `commit-reset` | Sin regla adicional | Declarado, sin regla adicional | Misma firma que `commit-set` | Conservar la presencia distinta de la regla |
| Referencia oficial | Contrato pertinente | Consecuencia para UIX |
| ------------------------------------------------------------------------ | --------------------------------------- | ----------------------------------------------------- |
| [WAI-ARIA APG Slider](https://www.w3.org/WAI/ARIA/apg/patterns/slider/) | Thumb con valor accesible y teclado | No tocar Morfo ni interacción |
| [Ark UI ColorPicker](https://ark-ui.com/docs/components/color-picker) | Area, sliders y fin de cambio separados | Mantener el commit distinto del drag |
| [Chakra ColorPicker](https://chakra-ui.com/docs/components/color-picker) | Control, area y canales componibles | No unir providers ni superficies |
| [React Aria ColorArea](https://react-aria.adobe.com/ColorArea) | Área de dos ejes con estado accesible | La política Sema puede coincidir sin compartir estado |
Morfo conserva partes, roles, valores ARIA y eventos; Soma conserva estado,
pointer, teclado y emisiones; Sema posee únicamente las firmas de las reglas;
Eidos no cambia. No se añaden props, tamaños, variantes, colores, partes, canales,
demos ni API pública. Los sliders internos de ColorPicker y GradientBuilder siguen
aportando sus propias señales; no se duplican en los packs padres.
## Hipótesis de extracción
Las dos primeras reglas son iguales en los tres packs salvo el selector. Las
reglas `commit-set` de ColorPicker y GradientBuilder son iguales entre sí, y
GradientBuilder repite esa firma para `commit-reset`. Dos constructores privados
de reglas Sema pueden eliminar las copias dejando las llamadas a `semaSelector`
en cada pack. La prueba de caracterización fijará los selectores, la lista
completa de reglas y su orden antes de modificar producción. Si la salida o el
tipado cambian, se abandona la extracción.
## Resultado de esta revisión
La prueba de caracterización de los tres packs pasa 170/170 junto con el censo,
resolver y engine. `component:audit` da PASS para Slider y GradientBuilder,
pero ColorPicker queda en NEEDS-WORK por un error previo `A-3.7`: ocho acciones
de teclado mutantes frente a cuatro eventos semánticos; registra además el aviso
`D-1.2` sobre la demo congelada. Ninguno procede de Sema. Corregir el primero
requiere una decisión de contrato Morfo/Soma y el segundo afectaría `web/`, fuera
del alcance. **No se extraen estas reglas en esta fase.** La caracterización
queda como referencia para una revisión posterior; no hubo cambios de producción
en esta familia.

@ -0,0 +1,58 @@
---
title: UIX — auditoría Sema de CssField y NumberField
type: notes
audience: human + agent
authority: process — evaluación, no contrato de componente
status: reviewed
---
# Firma Sema de CssField y NumberField
Fecha: 2026-09-23. Se releyeron las cuatro guías de componente de `AGENTS.md`,
los Morfos, packs y emisiones de ambos providers. La autorización del usuario
cubre sólo deduplicación interna sin cambiar comportamiento; `web/` queda fuera.
## Pre-flight
| Aspecto | NumberField | CssField | Decisión |
| --------------------- | ---------------------------------------------- | -------------------------------------- | ----------------------------------- |
| `commit-set` | Haptic `tap` en Provider | Idéntico | Una regla compartida |
| `handle-pick` | `air`, tick 0.18/6, sound + haptic en Scrubber | Idéntico | Una regla compartida |
| `handle-drag-scrub` | Tick 0.08/4, sólo haptic en Scrubber | Idéntico | Conservar el canal restringido |
| `signal-warn-invalid` | No existe | Warning haptic en Input | Conservar la regla local y al final |
| Estado y valor | Número y límites | Número, unidad y validación de formato | Providers separados |
| Referencia oficial | Contrato relevante | Consecuencia para UIX |
| ------------------------------------------------------------------------------- | ----------------------------------------- | ----------------------------------- |
| [WAI-ARIA APG Spinbutton](https://www.w3.org/WAI/ARIA/apg/patterns/spinbutton/) | Edición y teclado en el campo, valor ARIA | No cambiar Morfo ni foco |
| [Ark UI Number Input](https://ark-ui.com/docs/components/number-input) | Input y controles de incremento separados | No unir estructura ni providers |
| [Chakra Number Input](https://chakra-ui.com/docs/components/number-input) | Scrubber y steps como superficies propias | Compartir sólo firma Sema |
| [React Aria NumberField](https://react-aria.adobe.com/NumberField) | Valor, teclado y controles accesibles | Mantener la gestión de estado local |
Morfo conserva roles, eventos, target, teclado y ARIA; Soma conserva valores,
unidades, validación, temporización y emisión; Sema sólo construye las tres
reglas idénticas con los selectores tipados de cada Morfo; Eidos no cambia.
No se añaden partes, props, tamaños, variantes, colores, canales, demos ni API.
## Hipótesis de extracción
Los packs repiten exactamente las tres primeras reglas en el mismo orden. Un
helper privado puede recibir `onProvider` y `onScrubber` de cada pack y devolver
esas tres reglas. CssField mantendrá su cuarto selector `onInput` para la señal
de valor inválido. Antes de modificar producción se fijará la cascada completa
de ambos packs en una prueba; después se comparará la misma salida y se
ejecutarán los tests de providers, el censo Sema, la auditoría dirigida y el gate.
## Resultado
La línea base pasó 191/191 tests en seis archivos (cascada, censo, resolver,
engine y ambos providers); las dos auditorías dirigidas dieron PASS. El helper
`components/internal/spin-field-cascade.ts` recibe los constructores locales de
selectores, por lo que la comprobación de eventos y partes sigue dependiendo del
Morfo de cada pack. NumberField usa las tres reglas; CssField las coloca antes de
su advertencia local. La misma suite dirigida volvió a pasar 191/191.
El diff de producción añade 29 líneas y retira 34: **cinco líneas físicas netas
menos**. El beneficio principal es una sola definición de las tres firmas y
su orden, sin alterar eventos, targets, temporización, DOM ni CSS. No se hizo
revisión visual manual porque esta extracción no toca render ni receta.

@ -1,6 +1,7 @@
import { semaSelector } from '$uix/morfo';
import { cssFieldMorfo } from '$uix/morfo/components/css-field';
import type { Sema } from '../sema-map';
import { spinFieldCascade } from './internal/spin-field-cascade';
/**
* CssField perceptual defaults.
@ -25,22 +26,7 @@ const onScrubber = (matchers?: Parameters<typeof semaSelector<typeof cssFieldMor
export const cssFieldSema: Sema = {
name: 'css-field',
cascade: [
{
selector: onProvider({ eventName: 'commit-set' }),
haptic: { kind: 'tap' }
},
{
selector: onScrubber({ eventName: 'handle-pick' }),
channels: ['sound', 'haptic'],
sound: 'air',
haptic: { kind: 'tick', intensity: 0.18, duration: 6 }
},
{
selector: onScrubber({ eventName: 'handle-drag-scrub' }),
channels: ['haptic'],
haptic: { kind: 'tick', intensity: 0.08, duration: 4 }
},
...spinFieldCascade(onProvider, onScrubber),
{
selector: onInput({ eventName: 'signal-warn-invalid' }),

@ -0,0 +1,25 @@
import type { Sema } from '../../sema-map';
type OnProvider = (matchers: { eventName: 'commit-set' }) => string;
type OnScrubber = (matchers: { eventName: 'handle-pick' | 'handle-drag-scrub' }) => string;
/** Shared spin-field signature; each pack supplies selectors from its own morfo. */
export function spinFieldCascade(onProvider: OnProvider, onScrubber: OnScrubber): Sema['cascade'] {
return [
{
selector: onProvider({ eventName: 'commit-set' }),
haptic: { kind: 'tap' }
},
{
selector: onScrubber({ eventName: 'handle-pick' }),
channels: ['sound', 'haptic'],
sound: 'air',
haptic: { kind: 'tick', intensity: 0.18, duration: 6 }
},
{
selector: onScrubber({ eventName: 'handle-drag-scrub' }),
channels: ['haptic'],
haptic: { kind: 'tick', intensity: 0.08, duration: 4 }
}
];
}

@ -1,6 +1,7 @@
import { semaSelector } from '$uix/morfo';
import { numberFieldMorfo } from '$uix/morfo/components/number-field';
import type { Sema } from '../sema-map';
import { spinFieldCascade } from './internal/spin-field-cascade';
/**
* NumberField perceptual defaults.
@ -20,22 +21,5 @@ const onScrubber = (matchers?: Parameters<typeof semaSelector<typeof numberField
export const numberFieldSema: Sema = {
name: 'number-field',
cascade: [
{
selector: onProvider({ eventName: 'commit-set' }),
haptic: { kind: 'tap' }
},
{
selector: onScrubber({ eventName: 'handle-pick' }),
channels: ['sound', 'haptic'],
sound: 'air',
haptic: { kind: 'tick', intensity: 0.18, duration: 6 }
},
{
selector: onScrubber({ eventName: 'handle-drag-scrub' }),
channels: ['haptic'],
haptic: { kind: 'tick', intensity: 0.08, duration: 4 }
}
]
cascade: spinFieldCascade(onProvider, onScrubber)
};

@ -0,0 +1,74 @@
import { describe, expect, it } from 'vitest';
import { semaSelector } from '$uix/morfo';
import { colorPickerMorfo } from '$uix/morfo/components/color-picker';
import { gradientBuilderMorfo } from '$uix/morfo/components/gradient-builder';
import { sliderMorfo } from '$uix/morfo/components/slider';
import { colorPickerSema } from './components/color-picker';
import { gradientBuilderSema } from './components/gradient-builder';
import { sliderSema } from './components/slider';
describe('direct manipulation perceptual packs', () => {
it('keeps Slider pickup and drag on its provider', () => {
expect(sliderSema.cascade).toEqual([
{
selector: semaSelector(sliderMorfo, 'provider', { eventName: 'handle-pick' }),
channels: ['sound', 'haptic'],
sound: 'air',
haptic: { kind: 'tick', intensity: 0.22, duration: 6 }
},
{
selector: semaSelector(sliderMorfo, 'provider', { eventName: 'handle-drag' }),
channels: ['sound', 'haptic']
}
]);
});
it('keeps ColorPicker pickup/drag on area and release on provider', () => {
expect(colorPickerSema.cascade).toEqual([
{
selector: semaSelector(colorPickerMorfo, 'area', { eventName: 'handle-pick' }),
channels: ['sound', 'haptic'],
sound: 'air',
haptic: { kind: 'tick', intensity: 0.22, duration: 6 }
},
{
selector: semaSelector(colorPickerMorfo, 'area', { eventName: 'handle-drag' }),
channels: ['sound', 'haptic']
},
{
selector: semaSelector(colorPickerMorfo, 'provider', { eventName: 'commit-set' }),
channels: ['sound', 'haptic'],
sound: 'settle',
haptic: { kind: 'tick', intensity: 0.18, duration: 8 }
}
]);
});
it('keeps GradientBuilder pickup/drag on track and both releases on provider', () => {
expect(gradientBuilderSema.cascade).toEqual([
{
selector: semaSelector(gradientBuilderMorfo, 'track', { eventName: 'handle-pick' }),
channels: ['sound', 'haptic'],
sound: 'air',
haptic: { kind: 'tick', intensity: 0.22, duration: 6 }
},
{
selector: semaSelector(gradientBuilderMorfo, 'track', { eventName: 'handle-drag' }),
channels: ['sound', 'haptic']
},
{
selector: semaSelector(gradientBuilderMorfo, 'provider', { eventName: 'commit-set' }),
channels: ['sound', 'haptic'],
sound: 'settle',
haptic: { kind: 'tick', intensity: 0.18, duration: 8 }
},
{
selector: semaSelector(gradientBuilderMorfo, 'provider', { eventName: 'commit-reset' }),
channels: ['sound', 'haptic'],
sound: 'settle',
haptic: { kind: 'tick', intensity: 0.18, duration: 8 }
}
]);
});
});

@ -0,0 +1,57 @@
import { describe, expect, it } from 'vitest';
import { semaSelector } from '$uix/morfo';
import { cssFieldMorfo } from '$uix/morfo/components/css-field';
import { numberFieldMorfo } from '$uix/morfo/components/number-field';
import { cssFieldSema } from './components/css-field';
import { numberFieldSema } from './components/number-field';
describe('spin field perceptual packs', () => {
it('keeps NumberField commit and scrub signatures on their own targets', () => {
expect(numberFieldSema.cascade).toEqual([
{
selector: semaSelector(numberFieldMorfo, 'provider', { eventName: 'commit-set' }),
haptic: { kind: 'tap' }
},
{
selector: semaSelector(numberFieldMorfo, 'scrubber', { eventName: 'handle-pick' }),
channels: ['sound', 'haptic'],
sound: 'air',
haptic: { kind: 'tick', intensity: 0.18, duration: 6 }
},
{
selector: semaSelector(numberFieldMorfo, 'scrubber', {
eventName: 'handle-drag-scrub'
}),
channels: ['haptic'],
haptic: { kind: 'tick', intensity: 0.08, duration: 4 }
}
]);
});
it('keeps CssField warning after the identical commit and scrub signatures', () => {
expect(cssFieldSema.cascade).toEqual([
{
selector: semaSelector(cssFieldMorfo, 'provider', { eventName: 'commit-set' }),
haptic: { kind: 'tap' }
},
{
selector: semaSelector(cssFieldMorfo, 'scrubber', { eventName: 'handle-pick' }),
channels: ['sound', 'haptic'],
sound: 'air',
haptic: { kind: 'tick', intensity: 0.18, duration: 6 }
},
{
selector: semaSelector(cssFieldMorfo, 'scrubber', {
eventName: 'handle-drag-scrub'
}),
channels: ['haptic'],
haptic: { kind: 'tick', intensity: 0.08, duration: 4 }
},
{
selector: semaSelector(cssFieldMorfo, 'input', { eventName: 'signal-warn-invalid' }),
haptic: { kind: 'warning' }
}
]);
});
});
Loading…
Cancel
Save

Powered by TurnKey Linux.