Módulo 7: Primitives And Component Libraries
Proyecto: el carrito y el orden de Mercado sobre primitivas accesibles
Descripción
Las siete lecciones anteriores te dieron, en orden, todo lo que hace falta: por qué el comportamiento accesible es invisible y fácil de olvidar (L1-L2), cómo está hecha una primitiva por dentro (L3), cómo se viste con tus tokens (L4), de dónde sale el código que la viste (L5), cuándo te conviene usarla (L6), y el caso más exigente auditado a fondo, con números (L7). Este proyecto te pone a construirlo sobre el caso real de Mercado: el carrito (hoy, un ícono que probablemente no abre nada todavía en tu storefront) se vuelve un Dialog de verdad, y el orden del catálogo ("Ordenar por: precio, relevancia...") se vuelve un DropdownMenu de verdad. Los dos sobre primitivas de Radix, los dos vestidos con variants() y los tokens de Mercado que dominas desde el módulo 5, y los dos auditados con el mismo a11yAudit() que usaste en las lecciones 1, 2 y 7 —sin ningún cambio en su forma—.
El entregable tiene tres partes. La parte 1 audita el CartDialog: casero contra sobre-primitiva, seis casillas, el mismo patrón exacto de la lección 7. La parte 2 hace lo mismo con el SortMenu, pero con un subconjunto de casillas distinto —aquí sí entran las flechas, porque un menú de opciones sí se navega con ellas, y no entra el focus-trap, porque un menú no es modal—. La parte 3 viste las dos primitivas con la config de Mercado, probando que "vestir" no cambió entre un Dialog y un DropdownMenu: es el mismo variants() de siempre.
Conexión con el módulo. Este proyecto es la síntesis de las ocho lecciones. Aplica a11yAudit() (L1, L2, L7) a dos componentes reales de Mercado en vez de a ejemplos genéricos. Usa la anatomía compuesta (L3) y el patrón asChild para montar el trigger sobre el Button del módulo 5. Viste con variants() (L4) exactamente como en el ejemplo de la lección 4. Y las dos primitivas —Radix Dialog y Radix DropdownMenu— llegan a tu package.json como dependencia real, con su estilo copiado y editable (L5), porque el motor de decisión de la lección 6 las marcó sin dudar: las dos combinan teclado no trivial y manejo de foco. Es también el cierre de la capa de comportamiento accesible sobre las capas de tokens, utilidades, escalas, componentes y responsive/dark de los módulos 2-6: el storefront de Mercado, completo, verificado.
Una analogía: la inspección antes de abrir al público
Antes de que un restaurante nuevo abra sus puertas, alguien recorre el local con una lista de verificación que no tiene nada que ver con la decoración: ¿la puerta de emergencia abre hacia afuera sin necesitar llave? ¿la alarma de incendio suena cuando se prueba? ¿el pasillo hacia la salida tiene el ancho mínimo, sin obstáculos? Ninguna de esas preguntas se responde mirando el local terminado y bonito —el mármol de la barra, la iluminación, el menú impreso—. Se responden probando cada mecanismo, uno por uno, con la lista en la mano. Y la razón de tener la lista no es desconfiar del arquitecto: es que un local puede verse perfecto y aun así tener una puerta de emergencia que no abre, y esa combinación —se ve bien, falla donde importa— es exactamente la que un incendio real pone a prueba, cuando ya es tarde para corregirla.
Este proyecto es esa inspección, aplicada al carrito y al menú de orden de Mercado antes de que lleguen a producción. a11yAudit() es la lista de verificación: no juzga si el CartDialog se ve elegante —eso ya lo resolviste con variants() en la parte 3—; prueba, casilla por casilla, si el mecanismo de foco, teclado y rol funciona cuando alguien lo necesita de verdad. Y como en la analogía, la lista se corre antes de que el componente se repita por todo el storefront, no después de que un usuario real —el equivalente al incendio— descubra que la puerta no abre.
Qué vas a construir
El entregable es un archivo de Node —mercado-a11y.js— que, al correrse, imprime tres bloques:
- La auditoría del
CartDialog(parte 1): casero contra sobre-primitiva, con el score y qué falta en cada uno. - La auditoría del
SortMenu(parte 2): el mismo patrón, con las casillas que le aplican a un menú de opciones en vez de a un modal. - Las dos primitivas vestidas con los tokens de Mercado (parte 3): la cadena de clases final de
CartDialog.ContentySortMenu.Content, resuelta convariants().
Como en el resto del módulo, el JSX real —Dialog.* y DropdownMenu.* de Radix, con asChild y las clases de Mercado— se muestra: es lo que escribirías en el storefront. La auditoría y la resolución de estilo se ejecutan, porque son la parte que se verifica con números, no con la vista.
Especificación del proyecto
Tu mercado-a11y.js debe cumplir esto:
Parte 1 — el CartDialog.
- Define
a11yAudit(component)con las siete casillas del módulo (role,focusable,activate,escape,arrows,focusTrap,ariaState), tal como en la lección 7. - Define
homemadeCartconapplicable: ['role', 'focusable', 'activate', 'escape', 'focusTrap', 'ariaState'](sinarrows: unDialogno tiene lista que navegar) ypass: ['focusable', 'activate'](el trigger es un botón real; el panel, no). - Define
primitiveCartcon el mismoapplicableypasscompleto. - Imprime el score y qué falta de cada uno.
Parte 2 — el SortMenu.
- Define
homemadeSortconapplicable: ['role', 'focusable', 'activate', 'escape', 'arrows', 'ariaState'](sinfocusTrap: un menú no es modal) ypass: ['focusable', 'activate']. - Define
primitiveSortcon el mismoapplicableypasscompleto. - Imprime el score y qué falta de cada uno.
Parte 3 — vestir las dos primitivas.
- Reusa
variants(config, props)del módulo 5, sin modificarlo. - Define
cartContentVariants(fondo, texto, sombra, un ejesize) ysortMenuContentVariants(fondo, texto, borde conborder-primary, un ejesize) con los tokens de Mercado. - Imprime la cadena de clases resuelta para cada uno.
Restricciones (las convenciones del módulo):
- Todo identificador, prop, clase, config y valor en inglés; solo comentarios y textos en español.
- Sin dependencias: puro JavaScript, corre con
node mercado-a11y.js. - Salida literal y reproducible.
a11yAudit()yvariants()no cambian de forma respecto a como se definieron en L7 y L4/M5 — solo cambian los datos que les pasas.
Solución de referencia
Aquí está una solución completa que cumple la especificación. Estúdiala después de intentarlo por tu cuenta; el valor del proyecto está en construirlo tú, no en leer la respuesta:
// L8 proyecto - el carrito (Dialog) y el orden (DropdownMenu) de Mercado, sobre primitivas
// accesibles y vestidos con variants() (M5) + tokens (M2). a11yAudit() es el mismo checklist
// de L1/L2/L7, sin cambios; solo cambia el widget que le pasas.
function variants(config, props = {}) {
const classes = config.base ? [config.base] : [];
for (const axis of Object.keys(config.variants ?? {})) {
const value = props[axis] !== undefined ? props[axis] : config.defaultVariants?.[axis];
const cls = config.variants[axis]?.[value];
if (cls) classes.push(cls);
}
return classes.join(' ');
}
const CHECKS = [
{ key: 'role', label: 'rol ARIA correcto' },
{ key: 'focusable', label: 'el trigger es focuseable con Tab' },
{ key: 'activate', label: 'Enter/Space en el trigger abre' },
{ key: 'escape', label: 'Escape cierra' },
{ key: 'arrows', label: 'flechas navegan entre opciones' },
{ key: 'focusTrap', label: 'foco atrapado dentro mientras esta abierto' },
{ key: 'ariaState', label: 'expone estado por aria-* (aria-expanded, aria-modal...)' },
];
function a11yAudit(component) {
const applicable = CHECKS.filter((c) => component.applicable.includes(c.key));
const missing = applicable.filter((c) => !component.pass.includes(c.key)).map((c) => c.label);
return { name: component.name, score: applicable.length - missing.length, total: applicable.length, missing };
}
// ---------- parte 1: el carrito (CartDialog) ----------
const homemadeCart = {
name: 'CartDialog casero',
applicable: ['role', 'focusable', 'activate', 'escape', 'focusTrap', 'ariaState'],
pass: ['focusable', 'activate'],
};
const primitiveCart = {
name: 'CartDialog sobre Dialog primitivo',
applicable: ['role', 'focusable', 'activate', 'escape', 'focusTrap', 'ariaState'],
pass: ['role', 'focusable', 'activate', 'escape', 'focusTrap', 'ariaState'],
};
console.log('=== 1) el carrito de Mercado: CartDialog casero vs sobre una primitiva ===\n');
for (const c of [homemadeCart, primitiveCart]) {
const r = a11yAudit(c);
console.log(`${r.name}: ${r.score}/${r.total}`);
if (r.missing.length) console.log(' falta: ' + r.missing.join(', '));
}
// ---------- parte 2: el orden (SortMenu) ----------
const homemadeSort = {
name: 'SortMenu casero',
applicable: ['role', 'focusable', 'activate', 'escape', 'arrows', 'ariaState'],
pass: ['focusable', 'activate'],
};
const primitiveSort = {
name: 'SortMenu sobre DropdownMenu primitivo',
applicable: ['role', 'focusable', 'activate', 'escape', 'arrows', 'ariaState'],
pass: ['role', 'focusable', 'activate', 'escape', 'arrows', 'ariaState'],
};
console.log('\n=== 2) el orden de Mercado: SortMenu casero vs sobre una primitiva ===\n');
for (const c of [homemadeSort, primitiveSort]) {
const r = a11yAudit(c);
console.log(`${r.name}: ${r.score}/${r.total}`);
if (r.missing.length) console.log(' falta: ' + r.missing.join(', '));
}
// ---------- parte 3: vestir las dos primitivas con tokens de Mercado ----------
const cartContentVariants = {
base: 'fixed right-0 top-0 h-full w-full rounded-l-md bg-surface text-foreground shadow-lg p-6',
variants: { size: { sm: 'max-w-sm', md: 'max-w-md' } },
defaultVariants: { size: 'sm' },
};
const sortMenuContentVariants = {
base: 'rounded-md bg-surface text-foreground shadow-lg p-1 border border-primary',
variants: { size: { sm: 'w-40', md: 'w-56' } },
defaultVariants: { size: 'sm' },
};
console.log('\n=== 3) vestidas con los tokens de Mercado (variants(), M5) ===\n');
console.log('CartDialog.Content ->', variants(cartContentVariants, {}));
console.log('SortMenu.Content ->', variants(sortMenuContentVariants, { size: 'md' }));
Y así se ven las dos primitivas de Mercado como componentes reales, ya vestidas —esto se muestra, es tu entregable de marcado—:
// CartDialog.jsx — el carrito de Mercado, sobre Dialog.* de Radix + variants() de M5.
import * as Dialog from '@radix-ui/react-dialog';
import { cva } from 'class-variance-authority';
import { Button } from '@/components/ui/button'; // el Button del modulo 5
const cartContentVariants = cva(
'fixed right-0 top-0 h-full w-full rounded-l-md bg-surface text-foreground shadow-lg p-6 data-[state=open]:animate-in data-[state=closed]:animate-out',
{ variants: { size: { sm: 'max-w-sm', md: 'max-w-md' } }, defaultVariants: { size: 'sm' } }
);
function CartDialog({ items, total }) {
return (
<Dialog.Root>
<Dialog.Trigger asChild>
<Button variant="ghost" size="md">Carrito ({items.length})</Button>
</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay className="fixed inset-0 bg-black/50" />
<Dialog.Content className={cartContentVariants({ size: 'sm' })}>
<Dialog.Title className="text-lg font-medium">Tu carrito</Dialog.Title>
<Dialog.Description className="text-sm text-muted">{items.length} productos, ${total} MXN</Dialog.Description>
{/* ... lista de items ... */}
<Dialog.Close asChild>
<Button variant="primary" size="lg">Ir a pagar</Button>
</Dialog.Close>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
}
// SortMenu.jsx — el orden del catalogo de Mercado, sobre DropdownMenu.* de Radix + variants() de M5.
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
import { cva } from 'class-variance-authority';
import { Button } from '@/components/ui/button';
const sortMenuContentVariants = cva(
'rounded-md bg-surface text-foreground shadow-lg p-1 border border-primary',
{ variants: { size: { sm: 'w-40', md: 'w-56' } }, defaultVariants: { size: 'sm' } }
);
function SortMenu({ sort, onSortChange }) {
return (
<DropdownMenu.Root>
<DropdownMenu.Trigger asChild>
<Button variant="secondary" size="sm">Ordenar por: {sort}</Button>
</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content className={sortMenuContentVariants({ size: 'sm' })}>
<DropdownMenu.Item className="rounded-sm px-2 py-1 data-[highlighted]:bg-primary data-[highlighted]:text-white" onSelect={() => onSortChange('relevance')}>
Relevancia
</DropdownMenu.Item>
<DropdownMenu.Item className="rounded-sm px-2 py-1 data-[highlighted]:bg-primary data-[highlighted]:text-white" onSelect={() => onSortChange('price-asc')}>
Precio: menor a mayor
</DropdownMenu.Item>
<DropdownMenu.Item className="rounded-sm px-2 py-1 data-[highlighted]:bg-primary data-[highlighted]:text-white" onSelect={() => onSortChange('price-desc')}>
Precio: mayor a menor
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
);
}
Qué esperar. Al correr node mercado-a11y.js, la salida es exactamente esta:
=== 1) el carrito de Mercado: CartDialog casero vs sobre una primitiva ===
CartDialog casero: 2/6
falta: rol ARIA correcto, Escape cierra, foco atrapado dentro mientras esta abierto, expone estado por aria-* (aria-expanded, aria-modal...)
CartDialog sobre Dialog primitivo: 6/6
=== 2) el orden de Mercado: SortMenu casero vs sobre una primitiva ===
SortMenu casero: 2/6
falta: rol ARIA correcto, Escape cierra, flechas navegan entre opciones, expone estado por aria-* (aria-expanded, aria-modal...)
SortMenu sobre DropdownMenu primitivo: 6/6
=== 3) vestidas con los tokens de Mercado (variants(), M5) ===
CartDialog.Content -> fixed right-0 top-0 h-full w-full rounded-l-md bg-surface text-foreground shadow-lg p-6 max-w-sm
SortMenu.Content -> rounded-md bg-surface text-foreground shadow-lg p-1 border border-primary w-56
Lee la salida como el entregable que es, en sus tres partes.
La parte 1 confirma, con el CartDialog real de Mercado, exactamente lo que la lección 7 mostró con nombres genéricos: 2/6 contra 6/6. El casero pierde las mismas cuatro casillas por la misma razón —el panel es un <div> sin rol, sin Escape, sin focus-trap y sin aria-modal—, y el Dialog.* de Radix las resuelve todas. Este resultado no cambió por usar el caso de negocio real: a11yAudit() no le importa si el modal contiene un carrito o un formulario de contacto — mide comportamiento, no contenido.
La parte 2 es la novedad del proyecto: un widget con un subconjunto distinto de casillas aplicables. El SortMenu pierde arrows en vez de focusTrap —porque un menú de opciones sí necesita navegación con flechas entre "Relevancia", "Precio: menor a mayor" y "Precio: mayor a menor", y no necesita atrapar el foco porque no es un overlay modal que bloquea el resto de la página—. El score final es el mismo 2/6 contra 6/6, pero por un camino distinto: el casero de nuevo solo tiene focusable y activate (el trigger es un botón real), y le falta exactamente lo que un <div> con onClick por ítem nunca resuelve por sí solo: rol de menú, Escape para cerrar, flechas para moverse entre ítems, y el estado (aria-expanded en el trigger cuando el menú está abierto). Compara esto con la lección 7: mismo instrumento, mismo patrón de "el trigger está bien, el contenido no", pero la lista exacta de qué falta cambia según qué tipo de widget estás midiendo — la lección de la lección 1 sobre applicable se demuestra en un segundo caso, no solo el primero.
La parte 3 cierra el proyecto vistiendo las dos primitivas ya auditadas. CartDialog.Content sale con los tokens de superficie de Mercado, ancla a la derecha (right-0 top-0 h-full, un patrón de carrito lateral en vez de centrado) y el eje size en su default sm. SortMenu.Content sale con los mismos tokens de superficie pero un borde (border border-primary) que lo distingue como un menú flotante, en size: 'md' (w-56, más ancho que el default sm para que quepan las tres opciones de orden sin cortarse). Ninguna de las dos cadenas de clases tiene relación con las seis casillas de las partes 1 y 2 — es la prueba final de que comportamiento y apariencia, en las dos primitivas, siguen siendo capas completamente independientes, exactamente como estableció la lección 4.
Junta las tres partes y tienes el módulo 7 cerrado sobre el caso real: el carrito y el orden de Mercado, construidos sobre primitivas accesibles (comportamiento verificado con números, no con la vista), vestidos con el sistema de tokens y variantes que vienes construyendo desde el módulo 2, listos para entrar al storefront sin que nadie tenga que reimplementar un focus-trap a mano.
Extensiones (opcionales, para ir más lejos)
Si quieres exprimir el proyecto, prueba estas ampliaciones —cada una refuerza una lección del módulo—:
- Agrega un tercer widget:
Tooltip(L6). El motor de decisión de la lección 6 marcóTooltipcomo candidato a primitiva solo porhasFocusManagement, sin teclado complejo. DefinehomemadeTooltip/primitiveTooltipconapplicable: ['focusable', 'ariaState'](un tooltip no tiene rol de interacción propio ni se "activa"; se muestra al enfocar el elemento que describe) y confirma que el criterio de la lección 6 se sostiene con números. - Simula el
data-statede la parte 3 (L3). Agregadata-[state=open]:animate-inacartContentVariantsysortMenuContentVariants, y describe en un comentario qué animación dispararía cada uno al abrir — sin ejecutar nada nuevo, solo extendiendo la config con el patrón de la lección 3. - Audita el
Comboboxde búsqueda del header (L6, L7). Es el widget más complejo que el motor de la lección 6 mencionó y este proyecto no construyó. Define su checklist —applicableincluiríarole,focusable,activate,escape,arrowsyariaState(paraaria-expanded/aria-activedescendant)— y compáralo con elSortMenu: ¿qué casilla nueva tendría que aparecer que ninguno de los dos widgets de este proyecto necesitó? (Pista: un combobox además filtra opciones mientras escribes, lo que APG documenta con casillas de teclado extra.) - Edita el archivo copiado (L5). Cambia
cartContentVariants.basepara que el carrito de Mercado se abra centrado (fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2) en vez de lateral, como haría un rebrand real, y vuelve a correr la parte 3 para confirmar que el cambio no afecta ni una casilla de las partes 1 y 2 — la prueba definitiva de que estilo y comportamiento no se pisan.
Ninguna es necesaria para cumplir el proyecto; todas son buen entrenamiento para el resto de la guía.
Errores comunes
Copiar el applicable del Dialog para el DropdownMenu sin ajustarlo. Qué pasa: se reusa la lista ['role', 'focusable', 'activate', 'escape', 'focusTrap', 'ariaState'] del carrito para el menú de orden, sin cambiar focusTrap por arrows. Por qué pasa: los dos widgets se sienten parecidos —los dos son "algo que se abre desde un trigger"— y copiar la lista parece razonable. Cómo detectarlo: tu auditoría del SortMenu reporta que falta "foco atrapado dentro", una casilla que un menú de opciones nunca necesitó en primer lugar —confundes la ausencia de algo inaplicable con un fallo real—. Cómo corregirlo: antes de auditar un widget nuevo, pregúntate qué casillas le aplican a él, no al último que auditaste — un Dialog bloquea el resto de la página (necesita focusTrap, no arrows); un Menu de opciones no bloquea nada pero sí navegas sus ítems con flechas (necesita arrows, no focusTrap). Es exactamente la distinción que separó las partes 1 y 2 de este proyecto.
Vestir el SortMenu.Content tan angosto que el texto de las opciones se corta. Qué pasa: se deja size: 'sm' (w-40, 160px) para un menú cuya opción más larga es "Precio: mayor a menor", y el texto se desborda o se corta. Por qué pasa: sm es el default, y el default no siempre es el tamaño correcto para el contenido real. Cómo detectarlo: tu SortMenu vestido se ve apretado o el texto se corta en la opción más larga. Cómo corregirlo: como muestra la parte 3 de la solución de referencia, este proyecto eligió deliberadamente size: 'md' (w-56) para el SortMenu, distinto del default sm que sí le sirve al CartDialog — la variante correcta depende del contenido real, no de "usar siempre el default". Es el mismo criterio del módulo 5: los ejes existen para elegir, no para ignorar.
Pensar que auditar el SortMenu con a11yAudit() reemplaza probarlo con teclado de verdad. Qué pasa: se corre el script, se ve 6/6, y se da el componente por completamente verificado sin nunca abrirlo en el navegador. Por qué pasa: un número perfecto se siente como prueba suficiente. Cómo detectarlo: nadie del equipo navegó el SortMenu real, en el navegador, solo con teclado, antes de dar el proyecto por cerrado. Cómo corregirlo: recuerda la aclaración de la lección 2 — a11yAudit() es un modelo pedagógico con datos que tú describes a mano (pass: [...]); un 6/6 en tu script confirma que entendiste qué debía cumplir el componente, no que Radix esté mal configurado o que tu JSX tenga algún error de integración. La verificación final, en un componente real, siempre incluye abrirlo y navegarlo sin mouse — el script es la lista de preguntas; el navegador da las respuestas reales.
Rúbrica de autoevaluación
Marca cada punto; si todos están, dominaste el módulo:
-
a11yAudit()sin cambios de forma. Las siete casillas (role,focusable,activate,escape,arrows,focusTrap,ariaState) están definidas igual que en L1/L2/L7; solo cambian los datos que le pasas. -
applicablecorrecto por widget. ElCartDialogincluyefocusTrapy excluyearrows; elSortMenuincluyearrowsy excluyefocusTrap— cada widget medido contra lo que realmente le aplica. - El trigger casero está bien construido. Tanto
homemadeCartcomohomemadeSortpasanfocusableyactivate(son botones reales) — el fallo está en el contenido, no en el trigger, como en la lección 7. - Las dos primitivas pasan completo.
primitiveCartyprimitiveSortsacan el score máximo de suapplicable. -
variants()sin modificar. El motor de la parte 3 es idéntico al del módulo 5 y la lección 4 — ningún cambio de lógica, solo configs nuevas. - Las dos configs usan tokens de Mercado.
bg-surface,text-foreground,border-primary— no colores sueltos fuera del sistema. - Salida literal. Corriste
node mercado-a11y.jsde verdad y la salida coincide con lo que reportas —no inventaste el output—.
Resumen y cierre del módulo
Con este proyecto cerraste el módulo 7 haciendo, no solo leyendo. Construiste el CartDialog y el SortMenu de Mercado sobre primitivas de Radix, y verificaste su comportamiento accesible con números: los dos caseros sacaron 2/6 —el trigger bien hecho, el contenido roto—, los dos sobre primitivas sacaron el score completo de su checklist, sin que escribieras una línea de manejo de foco, teclado o rol. Luego los vestiste con variants() y los tokens de Mercado —un carrito lateral, un menú con borde— probando que la apariencia siguió siendo 100% tuya, sin que tocar un color pusiera en riesgo el comportamiento verificado. Con la inspección antes de abrir al público viste por qué se hace así: verificas el mecanismo, no solo la decoración, antes de que el componente se repita por todo el storefront.
Da un paso atrás y mira lo que aprendiste en las ocho lecciones. Sabes que la accesibilidad de comportamiento es invisible desde la pantalla —vive en un árbol paralelo que la apariencia no toca (L1-L2)—. Sabes que una primitiva es un kit de piezas con nombre, cada una con una responsabilidad de comportamiento exacta y cero clases de CSS (L3). Sabes que vestirla usa el mismo variants() que ya dominabas, sin una API nueva (L4). Sabes que "copiar el código" (shadcn) te da un archivo tuyo, editable sin fork, mientras la primitiva sin estilo sigue siendo una dependencia real que se actualiza sola (L5). Sabes decidir, con dos señales, cuándo construir y cuándo apoyarte en una primitiva (L6). Y auditaste, con números —no con la vista—, el widget más exigente del módulo, encontrando exactamente dónde se rompe un modal casero (L7). La capa de comportamiento accesible, sobre los tokens, utilidades, escalas, componentes y responsive/dark de los módulos 2-6, ya no es teoría: el carrito y el orden de Mercado funcionan para quien no usa mouse, verificado.
Lo que no hiciste todavía —a propósito— es juntar todo el sistema en un solo lugar. Construiste tokens (M2), los consumiste con utilidades (M3), les diste escalas y contraste medido (M4), los empaquetaste en componentes con variantes (M5), los hiciste responder al tamaño y al tema (M6), y ahora les diste comportamiento accesible heredado de primitivas (M7) — pero cada pieza vivió en su propio módulo, con su propio ejemplo. El módulo 8 — el capstone te pone a construir el sistema de diseño completo de Mercado de punta a punta, en un solo proyecto: tokens light/dark, Tailwind configurado con ellos, el Button y el ProductCard con variantes, contraste verificado, responsive y dark sin duplicar, y las primitivas accesibles que este módulo te enseñó a vestir — todo junto, todo ejecutado, todo el storefront de Mercado como un sistema, no como piezas sueltas.
Recursos
- Radix Primitives, "Dropdown Menu" — radix-ui.com/primitives/docs/components/dropdown-menu. La API completa de
DropdownMenu.*que usa elSortMenude este proyecto. En inglés. - W3C WAI-ARIA APG, "Menu Button Pattern" — w3.org/WAI/ARIA/apg/patterns/menu-button. El patrón oficial de teclado (flechas,
Home/End, tipo de letra para saltar) que las casillasarrowsyariaStatedelSortMenuresumen. En inglés. - shadcn/ui, "Dialog" y "Dropdown Menu" — ui.shadcn.com/docs/components/dialog y ui.shadcn.com/docs/components/dropdown-menu. Los componentes de producción que este proyecto emula —el mismo par, Radix + Tailwind + tokens, generado con el modelo de la lección 5. En inglés.
- Radix Primitives, "Accessibility" — radix-ui.com/primitives/docs/overview/accessibility. El resumen, por primitiva, de qué comportamiento de la APG implementa cada una —la fuente de verdad detrás de cada
6/6de este proyecto. En inglés.