You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/docs/process/CONTINUE-blocks-commerce.md

339 lines
23 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# CONTINUE — Familia `commerce` del tier `blocks`
> **Kickoff para sesión nueva**: _«Lee `docs/process/CONTINUE-blocks-commerce.md`
> y continúa la fase que toque.»_
>
> **Estado 2026-08-10: DISEÑO CERRADO, CERO CÓDIGO ESCRITO.** Las decisiones
> D-COM.1…5 están firmadas por el usuario. La fase **C0** (dossier + plan de
> ejecución + grupo en el catálogo + entrada en `next-features.md`) está
> **pendiente**: este handoff es lo único que existe todavía.
---
## 0. Qué es esto y por qué existe
Una familia de **26 blocks de ecommerce** más **8 componentes de canon** y un
**art nuevo `$commerce`**, diseñada con el mismo método con que nació el tier:
analizar lo hecho → comparativa contra las referencias reales de cada
ecosistema → fijar suelo de paridad → producir la lista.
El punto de partida, en tres hechos:
1. **No existía una sola línea de dominio commerce en el repo.** Barrido de
`cart · checkout · product · sku · order · basket · storefront · payment`
sobre `src/ · web/ · scripts/`: nada. Lo único adyacente es el block
`pricing`, que es comparativa de planes SaaS y cuyo README declara
explícitamente que **no formatea moneda**. `billing` figuraba diferido en F5
de [`PLAN-blocks.md`](./PLAN-blocks.md).
2. **El canon ya resuelve casi todo lo que un catálogo commerce exige** (§3), y
por eso la lista de huecos es corta y cada uno tiene motivo escrito.
3. **La doctrina de coordinación (2026-07-31) es justo lo que commerce pide.**
Hoy sólo 3 de 15 blocks coordinan y sólo 2 tienen máquina de estado.
Commerce invierte esa proporción — **14 de 26 coordinan** — porque casi todo
lo que se vende puede estar bloqueado. **El diseño arranca por el estado,
nunca por el layout.**
---
## 1. Puerta de lectura — antes de tocar nada
Precondición dura, no sugerencia. Leer **directo**, nunca delegando la lectura
a agentes.
- Siempre: `CLAUDE.md` (llega solo) · este handoff.
- Contrato del tier: [`architecture/blocks.md`](../architecture/blocks.md)
(B-1…B-11, la regla de admisión, la sección «Coordination»).
- Para un componente de canon: [`building-a-component.md`](../building-a-component.md)
(la ruta de 9 fases, LA puerta) + [`guides/component-guide.md`](../guides/component-guide.md)
(§Build contract + reglas A1–A37) + [`guides/completion-checklist.md`](../guides/completion-checklist.md)
(la matriz de aceptación).
- Vocabulario cerrado: [`canon/vocabularies.md`](../canon/vocabularies.md)
(generado del código — archetypes, familias, verbos, intents, holds).
- Contratos transversales: [`canon/tsc.md`](../canon/tsc.md) ·
[`canon/recipe-contract.md`](../canon/recipe-contract.md) ·
[`canon/direction-contract.md`](../canon/direction-contract.md).
- Para el ala agéntica: [`architecture/agent.md`](../architecture/agent.md)
(§5 el primitivo de actor, §7 la superficie de revisión).
- **Y el README de CADA componente que el block compone.** El mapa de
composición se escribe leyendo, no de memoria.
---
## 2. Las decisiones firmadas (D-COM.1…5, 2026-08-10)
| # | Decisión | Consecuencia operativa |
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **D-COM.1** | El estado del carrito vive en un **art nuevo `$commerce`** (`EngineCommerce`/`ActiveCommerce` + port de transporte que suministra el app), espejo de `$agent`/`AgentTransport` | Un carrito vive en cabecera, PDP, carrito y checkout: es estado **inter-block**, y B-10 prohíbe que un block importe otro. B-4 deriva `$<art>` de `src/arts/*`, así que la carpeta lo allowlista sola |
| **D-COM.2** | Alcance: alas **A+B+C+D+E** (26 blocks). Admin FUERA. **B2B diferida con disparador** | Admin lo cubre F3 (`data-table`, `dashboard`, `settings`), y ningún design system serio shippea commerce admin. B2B se registra en `next-features.md` |
| **D-COM.3** | La ley europea entra en el **TIPO**, más un token `jurisdiction` en el contexto comercial de `$commerce` | `priorPrice{amount, basis:'lowest-30d'}` obligatorio en rebaja · `taxIncluded` no opcional · `unit` cuando se vende por medida. No en `$prefs`: no es preferencia del usuario |
| **D-COM.4** | La procedencia por actor es **hilo, no apéndice** | `actor` nace en la línea de carrito en C2; el ala E la renderiza al final sin reabrir nada |
| **D-COM.5** | Grupo **«Commerce»** en `_lib/catalog.ts`; slugs sin prefijo salvo colisión | `cart`, no `commerce-cart`; pero `commerce-header`, igual que `site-header` |
**Dos cabos abiertos que resuelve la fase 0 de C2, no antes:**
1. ⚠️ **`$commerce` sería el primer art de DOMINIO.** Los 25 actuales son
infraestructura transversal (`adom`, `color`, `motion`, `prefs`…). O se
acepta explícitamente y se anota en `src/arts/README.md`, o se enmienda la
doctrina de arts. **No se cuela en silencio.** `src/svrs/commerce` no sirve:
B-4 no admite `$svrs` desde un block.
2. **El objeto de valor de dinero vive en una hoja por debajo del art y de
eidos**, para evitar el ciclo — mismo patrón que el `ActorToken`, cuyo tipo
vive bajo orca y agent mientras la autoridad de acuñación queda en
`EngineAgent`. Candidato: `src/libs/money`, junto al `src/libs/currency` que
ya existe.
---
## 3. Lo que el canon YA resuelve (no lo reinventes)
Medido sobre los 167 slugs de `src/uix/eidos/components/`, 100 con pila
completa morfo+soma+eidos:
| Necesidad | Se compone con |
| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Rejilla de producto | `AutoGrid` / **`CardGroup`** (ya es rejilla **seleccionable**) + `Card` + `AspectRatio` + `Image` + `Badge` |
| Estrellas | **`RatingGroup`** (`allowHalf · hoverPreview · clearable · readonly · valueText`) |
| Cantidad | **`NumberField`** (`min/max/step/largeStep · clampOnBlur · Increment/DecrementTrigger · Scrubber`) |
| Dinero | **`FormatNumber`** (`formatStyle="currency"`) + `Text numeric="tabular"` |
| **Divisa y conversión** | **`$format.currency`**: `setCurrency · format · formatAs · convert/convertAs` + `createRates({ fetchRate })`. **Ninguna referencia del mundo tiene esto** |
| Galería | `Carousel` + `Image` + `AspectRatio` (falta zoom/lightbox → §4) |
| Variantes | `RadioCards` · `ToggleGroup` · `ColorSwatch` (falta el eje de disponibilidad → §4) |
| Checkout | **`Stepper`** (`linear`) + `Form` + `Field` + **`Form.AutoFields`** + esquema SIUM |
| Carrito | `Drawer` + `Table`/`GridList` + `NumberField` + `EmptyState` |
| Facetas | `Accordion` + `CheckboxGroup` + **`Slider type="multiple"`** (ES el rango de precio) + `TagGroup` + `Combobox` |
| Pedidos | `Table` + `$libs/datagrid` — sorting multi/server · filtering · selection · pagination · visibility · pinning · row-detail. **Sin virtualización** |
| Seguimiento | **`Timeline`** (`align="alternate"`, `Marker`, `Connector`) |
| Stock | `Meter` (zonas `below/above/optimum`) + `Badge` |
| Estados | `EmptyState · Result · Callout · Skeleton · Toast · Announce` — **el cuarteto completo, que ningún catálogo del mercado tiene** |
| Envío/peso | `$format.units` (`length · mass · volume` + `convert`) |
| Retail | `Barcode` (EAN-13/UPC-A/ITF-14…) + `QrCode` |
| Búsqueda | `SearchField` (`debounceMs`, `loading`) · `Combobox` · `Command` |
---
## 4. Los 8 huecos de canon (fase C1)
Criterio único: **superficie de contrato** (evento real, máquina, `data-*` que
el CSS selecciona, obligación a11y de widget). Todo lo demás es composición y
va al block. Cada uno entra por la ruta de 9 fases.
| # | Componente | Por qué es canon |
| ---- | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| C1.1 | **`price`** | La ley vive en el tipo. Obligación a11y real: el tachado debe leerse «antes X, ahora Y», no «X Y» |
| C1.2 | **`variant-picker`** | El ítem de mayor valor de la comparativa. **Tres ejes booleanos por opción** (`exists`/`available`/`selected`, nomenclatura de Hydrogen) → eidos pinta las cuatro celdas de la matriz desde CSS sin ramas JS |
| C1.3 | **`quantity-field`** | Especialización fina **componiendo `NumberField`** (precedente sancionado: NumberField dentro de Knob). Aporta mínimo 1, **quitar al bajar de mínimo**, tope = stock, estado en vuelo, nombre accesible por producto |
| C1.4 | **`facets`** | La máquina, no las vistas. Data-driven (precedente: Menubar, NavTree). **Con la live region que Algolia nunca shippeó** — su `stats` renderiza el conteo como texto plano |
| C1.5 | **`lightbox`** | Ya era candidato de canon en F5 con disparador «cuando un block real de media/galería lo pida». Este es el disparador |
| C1.6 | **`address-field`** | Data-driven sobre metadatos públicos de país: conjunto, **orden**, **etiquetas**, regex postal y token `autocomplete` (WCAG 1.3.5). Requiere `$libs/address` |
| C1.7 | **`selection-set`** | Wishlist, comparar y lista de requisición B2B son **el MISMO proveedor**: conjunto sincronizado a URL, con tope, optimista. Construirlo una vez |
| C1.8 | **`embedded-field`** | Envoltorio de iframe de proveedor. **Nunca un input de tarjeta** — el PAN no entra en nuestro DOM. Aporta la resolución de tokens del tema al otro lado del límite vía `eidos.resolveToken()` |
**Enmiendas de canon, no componentes nuevos**: `RatingGroup` (hoy `readonly`
conserva `role="radio"` y cada estrella es parada de tabulación — diez ratings
en un listado son 50 paradas) · `Timeline`/`Stepper` (¿estado `error`? el
seguimiento de un pedido no es camino feliz) · `Card` (elevación + el
invariante del enlace de tarjeta entera) · `Badge` (`tone` que no dependa del
color) · `Table` (virtualización, gap conocido).
---
## 5. Los 26 blocks
**Ala A · descubrimiento (7)** — `commerce-header` · `product-grid` ·
`product-listing`◆ · `category-preview` · `promo-section` · `incentives` ·
`recommendation-slot`.
**Ala B · ficha (6)** — `product-gallery` · `buy-box`◆ · `product-info` ·
`reviews`◆ · `product-qa`◆ · `product-compare`◆.
**Ala C · carrito y pago (4)** — `cart`◆ · `order-summary`◆ · `checkout`◆ ·
`payment-methods`◆.
**Ala D · post-venta (6)** — `order-confirmation` · `order-tracking` ·
`order-history` · `order-detail` · `returns`◆ · `guest-order-lookup`◆.
**Ala E · agéntica (3)** — `cart-review`◆ · `cart-handoff`◆ ·
`purchase-authorization`◆.
◆ = coordina (máquina de estado + palabras). Son **14 de 26**.
**Ala F · B2B: DIFERIDA con disparador** — `quote-request` ·
`requisition-list` · `quick-order-pad` · aprobación de PO · pago a crédito ·
conmutador de cuenta/sede. `selection-set` (C1.7) ya la deja medio construida.
---
## 6. El patrón de coordinación — cópialo literal
`contact` y `newsletter` son los dos ejemplares vivos. La forma exacta:
- **`state.ts`** — unión exhaustiva de estados (cada miembro con su línea de
doc) · interfaz `…StateInput` con campos `readonly` · `resolve…State(input)`
como escalera de guardas **cuyo ORDEN es la prioridad** · `can…(state):
boolean` · **dos `Record<State, …>` exhaustivos**: `…_REASON:
Record<S, string | null>` y `…_ACTION: Record<S, string>`.
Como la clave es el tipo, **añadir un estado es un error de tipos hasta que
escribes su frase y su etiqueta**: un estado que bloquea sin decir por qué no
se puede escribir.
- **`context.ts`** — interfaz con **getters** (para que las lecturas sigan la
fuente reactiva), `reasonId`, `t(ref)`; `Symbol` privado del módulo;
`set…Context` / `get…Context` (el getter devuelve `| undefined` y cada parte
guarda).
- **`…-reason.svelte`** — pinta la frase **y la anuncia** por `uix.announce`
(el segundo servicio que el contrato B sanciona), **sólo al cambiar**, con un
`let announced = ''` que NO es `$state` (es un libro de efectos, no estado
reactivo). `aria-describedby` es mecanismo **aparte y aditivo**: asociación,
no anuncio, y se quieren los dos.
- **`state.test.ts`** — asserts **estructurales**, no caso a caso:
`ALL_STATES.filter(can…)` · «para todo estado bloqueante, `…_REASON` es
truthy» · el regex del idlangref `/^#\?blocks\.<slug>\.[a-z.]+\|.+/`.
- El estado se deriva **UNA vez en la raíz** y las partes lo leen. Ninguna
parte recalcula; ninguna inventa su propio `disabled`.
**Regla de forma del tier — no la re-decidas**: una parte compound se gana
sólo si **se repite** (el app mapea sobre N) o **coordina** (lee el contexto).
Si no hace ninguna de las dos, es un **slot de snippet** en la raíz.
**Las palabras** se registran en `web/routes/blocks/_lib/blocks-langs.ts` bajo
`blocks.{slug}.*` con `es`/`en`; el arnés las inyecta en `langs.schema` desde
`_lib/BootUix.svelte`. Sin registrar, sale el fallback inglés y no se rompe
nada.
---
## 7. Anatomía de fichero
```text
src/uix/blocks/{kebab}/
├── README.md # Function · Composition map · Decisions · Gaps
│ # + el párrafo **Landmark + headings** (B-8)
├── index.ts # export compound + tipos
├── types.ts # props; contenido por snippets
├── {kebab}.svelte # composición: sólo canon, layout por componentes, sin .css
├── {kebab}-{parte}.svelte
└── (si coordina) state.ts · state.test.ts · context.ts
web/routes/blocks/{kebab}/
├── +page.svelte # BlockDemo + controles vivos + pestañas de doc
├── {Name}Site.svelte # el block dentro de una página de contenido REAL
└── preview/
├── +layout@.svelte # `@` resetea el layout; BootUix; lee mode/dir/lang de la URL
└── +page.svelte # sirve {Name}Site desde searchParams
```
Y **flipar `shipped: true`** en `web/routes/blocks/_lib/catalog.ts`, dentro del
grupo «Commerce». El guard parsea ese fichero **por regex**
(`{ slug: '…', … shipped: true }`) y comprueba las **dos direcciones**: un
block sin ficha es invisible para el raíl; un slug publicado sin block es un
enlace muerto.
Los tipos: la raíz extiende `Omit<HTMLAttributes<HTMLElement>, 'children'>`;
**toda otra parte extiende los props del componente de canon que envuelve**,
nunca `HTMLAttributes` (los atributos crudos chocan con el `style`/`class`
refinados del canon). Cuando un slot comparte nombre con un atributo HTML
(`title`, `media`, `width`), se hace `Omit` — «la lección del hero».
---
## 8. Verificación
**Por componente de canon**: `npm run component:audit --only {kebab}` PASS ·
`npm run morfo:check` · `npm run morfo:vocabulary` · `eidos-lint` ·
`npx vitest run src/uix/eidos` · `npm run perm:check` · `npm run rtl:check`.
**Por block**: `npm run blocks:check` verde · `npx vitest run src/uix/blocks` ·
`npm run check` **sin regresión sobre baseline** (medir antes y después: el
baseline se mueve) · `npm run docs:check` · prettier.
**Navegador de verdad, y mirado**: claro y oscuro (`colorScheme: 'dark'`), 375
y 1280, **y RTL**. Sonda de audio + `MutationObserver` de `data-event*` tras la
puerta de hidratación honesta, **con un control en el mismo entorno** — sin
control, una latencia no es un veredicto.
**Acceptance propia de la familia**, además del contrato B:
1. **Cero cadenas horneadas.** La lista de ~20 props traducibles que
`ProductDetail` de BigCommerce Catalyst exige es la checklist: ninguna de
ellas existe aquí.
2. **El cuarteto de estados** en todo block con datos: poblado · cargando ·
vacío · error.
3. **Ningún estado bloqueante mudo** — la propiedad ya está fijada por test en
`contact`/`newsletter`; se hereda.
4. **Tabla de cumplimiento por block**, generada del morfo: qué criterios WCAG
y qué artículos satisface.
5. **Página compuesta de integración** por ala (listado → PDP → carrito →
checkout → confirmación).
---
## 9. Lo que muerde específicamente en commerce
- **RTL es EL eje donde commerce rompe**, y es un foso real: de doce catálogos
estudiados, exactamente uno anuncia RTL y no tiene ecommerce. Duele en el
orden antes/ahora del precio, la colocación del símbolo de divisa, los `−`/`+`
del stepper que **no** deben espejarse, el `scrollLeft` negativo del carrusel
y los chips de filtro.
- **Todo evento con consecuencia monetaria es `commit` con intent real**:
añadir = positivo, quitar = negativo con deshacer, seleccionar agotado =
`signal-threat`. Y el stepper suena por **repetición** (`step` por emisión),
así que la velocidad del gesto ES la velocidad del trinquete.
- **`Form.Submit` estampa su propio `aria-label` genérico**, que gana la
computación del nombre accesible: un botón con palabra de estado DEBE pasar
el suyo explícito o incumple WCAG 2.5.3. Medido en `contact` (A-04).
- **Bajo validación `progressive` se pregunta al ESQUEMA** (`validateSync`),
nunca a `form.isValid` — ahí significa «aún no se ha encontrado nada mal», y
un formulario vacío se declara válido. Y se **deja pasar `invalid`** al
submit, o el camino de error por campo es inalcanzable por construcción.
- **Deuda heredada que commerce va a encontrar otra vez**: las 27 filas todavía
CONFIRMADAS de [`AUDIT-blocks-ledger.md`](./AUDIT-blocks-ledger.md) y los 8
hallazgos de canon congelados (F15–F22 en
[`PLAN-blocks-quality.md`](./PLAN-blocks-quality.md)). Muerden aquí en
concreto: `Card` sin prop de elevación, la ranura `contrast` blanca sobre
todo escalón sólido, y el `aria-label` incondicional del canon.
- **Solape con F3**: `data-table` (F3.3) y `wizard` (F3.8) son parientes de
`order-history` y `checkout`. No bloquea — commerce compone `Table` +
`$libs/datagrid` y `Stepper` directamente, igual que haría F3 — pero cuando
F3 llegue habrá que decidir si `order-history` **es** `data-table`
configurado o su hermano.
---
## 10. Higiene de la comparativa
El dossier de referencia lleva **marcas de verificación** y hay que
respetarlas, por la misma razón que existe el ledger:
- **Verificado**: el inventario de Tailwind Plus Ecommerce (transcrito
categoría a categoría) · la ausencia total de commerce en Untitled UI, en los
headless y en los design systems · el desglose de Storefront UI (26
componentes npm / 24 bloques copy-paste) · la ausencia de blocks de ecommerce
en **todo** el ecosistema Svelte · las obligaciones legales de la UE.
- **NO verificado, y por tanto no citable**: las cifras por categoría de
Flowbite (su web es una SPA que devuelve cuerpo vacío) · todas las
estadísticas de Baymard (corpus de pago) · la penalización de conversión del
checkout en chat · los nombres de campo concretos de los esquemas de AP2.
**Regla**: una cifra sin fuente leída no entra en una tabla comparativa ni
justifica alcance por sí sola. Y **nunca se cita el total auto-declarado de un
catálogo** — se publica la enumeración y el número se deriva de ella. Se
observó dos veces que el total declarado y el enumerado no coinciden, y aplica
también a nuestras propias cifras.
---
## 11. Lo siguiente
**C0**, en este orden:
1. Escribir el dossier `docs/process/RESEARCH-commerce-references.md` con las
7 pistas y las marcas de §10.
2. Escribir el plan de ejecución `docs/process/PLAN-blocks-commerce.md` con la
ficha por ítem (Función · Compone · API · Layout/landmark · v1 · Demo).
3. Añadir el grupo «Commerce» a `web/routes/blocks/_lib/catalog.ts` con los 26
slugs en `shipped: false`.
4. Registrar la iniciativa en `docs/next-features.md` (+ el ala B2B diferida
con su disparador).
Luego **C2** (el art `$commerce`, cuya fase 0 resuelve los dos cabos de §2),
y sólo entonces **C1** — sus tipos los consume todo lo demás.

Powered by TurnKey Linux.