# RESEARCH — Estudio de referencia del tier blocks (paridad y superación) > Proceso, no doctrina. Alimenta las fases 0 de `PLAN-blocks.md`: cada fiche > de F1/F2/F3/F4 verifica contra este dossier en vez de investigar de cero. > Encargo del usuario (2026-07-21): *"estar al menos a la par y cuanto menos > superarlo"*. Método: 6 pistas de investigación web en paralelo sobre > fuentes oficiales vivas, cada una contrastando el plan contra el panorama > real y devolviendo brechas + ángulos de superación. **Estado**: ✅ COMPLETA 2026-07-21 — 6/6 pistas recibidas y sintetizadas. Enmiendas E-1…E-5 presentadas al usuario (ver §Síntesis; firmas se registran en `PLAN-blocks.md` §Registro). ## Rúbrica - **Suelo de paridad**: lo que ≥2 referencias convergen en ofrecer para un ítem (features, props, variantes, comportamientos). Un ítem nuestro no está "hecho" por debajo de su suelo salvo decisión explícita registrada. - **Diferenciadores**: features únicos de una referencia que valga la pena adoptar (con juicio, no por completismo). - **Superación**: lo que nuestra arquitectura permite y ninguna referencia tiene (contrato morfo introspectable, sema/percepción, tokens + density + RTL + dark de serie, composición real vs copy-paste). ## Pistas | # | Pista | Cubre | Estado | |---|---|---|---| | P1 | Catálogos de blocks de marketing | shadcn blocks · Tailwind Plus · Flowbite · PrimeBlocks · Relume · Untitled UI · daisyUI → F2 | lanzada | | P2 | Blocks de aplicación + app-shell | Tailwind Plus App UI · shadcn dashboard/sidebar/login · Mantine AppShell · Tremor · AntD ProComponents · Refine · auth UIs → F3 | lanzada | | P3 | Refs F1 (5 ligeros) | affix/anchor/empty/result/callout en AntD · Mantine · Radix Themes · Chakra · Polaris · GitHub/Docusaurus admonitions → F1.1–F1.4, F1.6 | lanzada | | P4 | Refs F1 (2 pesados) | prose (@tailwindcss/typography · Mantine · Primer) · sidebar (shadcn EXHAUSTIVO · AntD Sider · Mantine) → F1.5, F1.7 | lanzada | | P5 | Docs shells + docs blocks | Starlight · Fumadocs · Docusaurus · VitePress · Nextra · Mintlify · showcase/props-tables → F4 | lanzada | | P6 | Ecosistema Svelte competidor | shadcn-svelte · Bits · Melt · Skeleton · Flowbite-Svelte · svelte-ux · nuevos Svelte-5 → el listón "mejor de Svelte" | lanzada | ## Hallazgos ### P1 — Catálogos de marketing — ✅ RECIBIDA 2026-07-21 (conteos y nombres de variante en vivo) #### Panorama - **shadcn/ui blocks: CERO marketing** (solo sidebar/login/signup/dashboard) — el marketing vive en registries de terceros. Su aporte es el modelo de distribución (registry + CLI + "Open in v0"). - **Tailwind Plus**: ~133 ejemplos de marketing en 16 categorías (Heroes 12 · Features 15 · Pricing 12 · CTA 11 · Stats 8 · Testimonials 8 · FAQs 7 · Footers 7 · Newsletter 6 · Team 9 · Blog 7 · Contact 7 · Content 7 · Logo Clouds 6 · Bento 3 · Headers 8) + elements (Navbars 11 · Flyout 7 · **Banners 13**) — dump estático HTML/React/Vue; **dark = segundo artefacto a la venta**. - **Flowbite**: ~170 marketing en 28 categorías (Hero 18 con nombres capturados: carousel, phone-mockup+app-store, search-bar+datepicker, video, sign-up-in-hero…; auth y 404/500/maintenance clasificados como marketing). - **PrimeBlocks** (relanzado, Vue-first, $599/dev): ~168 marketing (Feature 27 · Testimonial 20 · Team 12 · Navbar 12 · CTA 12 · Logo Cloud 10…). - **Relume** (AI builder, 1.000+/plataforma): la taxonomía más ancha (31 categorías: gallery, events, careers, comparison, timeline, multi-step forms, cookie consent, link pages…); semántica débil ("Layout 249"). - **Untitled UI React**: el **casi-par más cercano** — componentes React reales con props (no dumps), la mayor profundidad por categoría (Hero 44 · Features 42 · Footers 40 · Testimonials 26 · Pricing 22 · FAQ 16 · Metrics 16) + 105 page examples; pero React-only, PRO-gated, sin tokens/RTL/density. - **daisyUI**: sin blocks (solo templates de pago de página completa) — confirma que ninguna librería class-based shippea secciones componibles. #### Suelo por bloque F2 (brechas concretas de nuestro v1) | Block | Suelo/recurrencias que nos faltan | |---|---| | hero | El esqueleto center\|split es correcto; la brecha es **tratamiento de media**, no conteo de layouts: presets de media (screenshot enmarcado / phone mockup / background-image cover), slot **form-in-hero** (email sign-up), **logo-strip** bajo las acciones, video embed. | | feature-grid | **LA brecha #1 de F2**: el **split/alternante texto-vs-screenshot con lista de features** lo shippean las 5 refs y es la sección nº1 tras el hero; nuestro v1 solo tiene el grid de iconos. | | testimonials | Falta la **cita única en spotlight** (logo + quote + avatar + autor) — la variante modal en TODAS las refs; el grid es minoría (2 de 8 en TW). | | faq | Falta la **lista estática 2/3-columnas** (mayoría del formato: 6 de 7 en TW no son acordeón) + la cola "Still have questions?" (CTA a soporte). | | pricing | Recurren y no tenemos: **tabla de comparación de features** y variante **single-price**. Toggle + featured = paridad OK. | | site-header | Sin **flyout/dropdown de navegación** no hay paridad (todas lo traen; nuestro NavigationMenu ya lo da — es cuestión de scope del block); y falta el hermano **announcement banner** (13 TW · 16 Untitled — nuestro componente Banner existe: candidato a block `banner` o slot del header). | | stats-band | Paridad OK; variantes recurrentes adoptables: split-with-image, timeline/stepped. **Ninguna ref puede shippear count-up** (markup estático) — nuestra superación literal. | | cta | Paridad con centered; añadir arreglo **justified** (texto izq., botones dcha.) y variante split-with-media. | | newsletter | Añadir slot de **nota de privacidad** bajo el field (convergente). | | site-footer | Añadir slots: **newsletter-form en footer** (3 de 7 TW) y **language selector** (encaja con nuestro langs). | | (ecosistema) | Todas las refs shippean **páginas compuestas** como artefacto de prueba (TW 10 · Untitled 105) — nuestra página de integración F2 cumple ese rol; considerar promocionarla a demo pública. | #### Categorías de la unión que NO planeamos (decisión explícita pendiente) ≥2 refs shippean: **banners** · logo-cloud (diferido ya) · **team** · blog · **contact** · content/prose-section · bento grid · flyout/mega-menu (elemento) · careers · gallery/portfolio · events · comparison section · timeline section · multi-step forms · cookie consent · popups/promos. Candidatas obvias F2.5+: banner (componente ya existe), team, contact, content-section (composición de `prose`+Section), bento (AutoGrid?). #### Superación ratificable Variantes como **props tipadas** vs N dumps duplicados; dark/brand automático por tokens vs segundo artefacto; **gestión de landmarks + política de heading-level** (categoría que la industria NI VENDE — los dumps hardcodean h-levels y rompen jerarquías al componer); comportamiento del canon (Accordion/Drawer/ToggleGroup) vs "Requires Flowbite JS"; count-up con reduced-motion; RTL + density + cero strings horneados (i18n by construction). ### P2 — Aplicación — ✅ RECIBIDA 2026-07-21 (conteos de catálogos vivos + APIs de repo) #### Panorama (los dos polos y el hueco del medio) - **Copy-paste sin API** (Tailwind Plus App UI ~40 categorías con conteos — Tables 19, Input Groups 21, Stacked Lists 15, Command Palettes 8, Empty States 6…; Flowbite ~155 blocks de app con taxonomía **CRUD-céntrica** (create/read/update/delete × form/modal/drawer/section — tratan "la pantalla CRUD" como unidad); Tremor Blocks 303 — señal de GRANULARIDAD: Filterbar/Table Actions/KPI Cards/Chart Tooltips como categorías de primer nivel; Mantine UI 123). Cero modelo de composición: 19 tablas = 19 snapshots divergentes sin lifecycle. - **Config-driven** (AntD ProComponents, Refine): todo props + escape-hatches `xxxRender` (~10 en ProLayout) → acantilado de personalización; ProTable = monolito search+table+toolbar infactorizable. - **shadcn blocks** = scaffold eject-and-own: `dashboard-01` copia 19 deps de registry + **6 paquetes npm a TU árbol** (tanstack-table, dnd-kit, recharts, zod…). 16 sidebars porque las variantes no pueden ser props. - **El hueco**: libertad de markup de Tailwind CON el comportamiento de ProTable = exactamente la API compuesta de partes con children. Nadie lo ocupa. #### Suelos de paridad por bloque F3 (brechas concretas de nuestro v1) | Block | Suelo que nos falta en v1 (decidir: subir alcance o registrar excepción) | |---|---| | app-shell | **Colapso desktop a icon-rail** (además de móvil→drawer; lo dan shadcn/Mantine/AntD); variante **stacked/topbar-only** (el primer fork en TODAS las refs); tamaños de slot responsive (`{base,sm,lg}` Mantine); scroll-hide del header; geometría de chrome como CSS vars (`--app-shell-*-offset` Mantine) para que el contenido se posicione contra ella. Skip-link: NINGUNA ref lo trae — superación nuestra. | | auth | Slot de **social providers**; slot de **error/mensaje** top-of-card (ProComponents `message`); prefill; estado de **carga/mounting** (Clerk `fallback`); la unión de vistas de Supabase es 6, no 4: falta **magic-link** y **update-password** (su `ViewType` = sign_in·sign_up·magic_link·forgotten_password·update_password·verify_otp — la enumeración más limpia del dominio). Nuestro OTP card SUPERA el suelo copy-paste (nadie lo shippea como block). | | data-table | El cuarteto estándar de toolbar ProTable = reload + **density** + column-setting + **fullscreen** (density/fullscreen a decidir explícito — density además es concern de framework en nuestro caso); **persistencia de layout de columnas** (`columnsState.persistenceKey`); **clear-selection** en la barra bulk (el triple `{selectedRowKeys, selectedRows, onCleanSelected}`); historia móvil de filtros (**faceted-search en drawer**, Flowbite). | | dashboard | Stat cards con **delta de tendencia** (up/down + tono) y **spark-chart embebido** (`chartPlacement` left/right/bottom); **selector de periodo** en panels de chart; arreglo fijo v1 OK (todas las refs lo fijan). Curiosidad adoptable: `StatisticCard.Group` con `Operation` ("=" "+" entre KPIs para expresar fórmulas). | | settings | Completo salvo: descripción por fila como slot formal; **feedback de éxito tras guardar** (Flowbite "Success Message" es categoría propia de 5). | | user-menu | Completo (tema/idioma SUPERA las refs — son auth-céntricas); falta `showName` (variante avatar+nombre) y la historia de **colocación en rail colapsado** (patrón nav-user de shadcn: flip a dropdown-up en pie de sidebar). | | notifications | ¡Ya estamos SOBRE el suelo — ninguna ref shippea el inbox completo como block nombrado! Añadir explícito: timestamps + distinción visual unread. | | wizard | Gate por paso **async** (promesa que resuelve false bloquea avance — StepsForm); `current` controlado para saltos del app; **stepper compacto móvil**; semántica de merge de valores entre pasos definida. | | error-page | Añadir variante **maintenance** (Flowbite la trata como página de primera); 403 nuestro SUPERA el suelo (casi ausente en refs). | | kanban | Slots de metadata en card (avatar/badge/due-date) + **add-card por columna**; toolbar de group-by/filtros = standout no-suelo (diferir dejando slot). Empty-state por columna: ninguna ref lo tiene — superación. shadcn NO tiene kanban block. | #### Superación (ratificable) + debilidades ajenas a evitar Lifecycle real (un fix de Table propaga a data-table/dashboard/kanban vs 19 snapshots); comportamiento heredado con CERO deps nuevas (vs 6 npm de dashboard-01); el punto medio composición-con-comportamiento; a11y a nivel block (skip-links, foco toolbar↔tabla↔paginación, live-regions "N seleccionados"/"todas leídas"/cambio de paso, roving en columnas kanban — NINGUNA ref lo shippea); theming/density/RTL/i18n del sustrato (todas hardcodean `dark:` e inglés); coherencia cross-block vía runtime compartido (sema en danger-zone/card-drop = canal que no existe fuera). **Evitar**: fork-and-drift (16 sidebars), monolito ProTable, Inferencer desechable, y la lección Supabase Auth UI: murió por appearance-API cerrada — nuestro auth como composición sobre handlers del app es la forma durable. ### P3 — F1 ligeros — ✅ RECIBIDA 2026-07-21 (APIs a nivel de prop; AntD 6.5 · Mantine 9.4 · Bootstrap 5.3 · Chakra v3 · Polaris · Atlaskit · Base UI 1.6) **Hueco headless verificado**: Base UI, Ark, React Aria y Radix Primitives shippean CERO de los cinco — seríamos los primeros headless+contrato en sticky y anchor-nav; los únicos comparables son kits estilizados (AntD) o generadores de sitios. #### sticky (F1.1) - **La plataforma ya fijó el vocabulario**: CSS `@container scroll-state(stuck: top|bottom)` (Chrome/Edge 133+; Firefox/Safari NO ≤2026). **Espejar exactamente** en `data-stuck` + `data-edge="top|bottom"` → los selectores eidos traducen mecánicamente al CSS nativo cuando aterrice; seríamos el único contrato stuck CSS-direccionable cross-browser (motor = doble centinela IO bendecido por Chrome, que además cumple nuestra doctrina anti-reflow). - **Brechas del boceto**: (1) **sin props de offset** — la geometría del centinela está acoplada matemáticamente al inset del sticky (el `-24px` del artículo de Chrome espeja el `top` del header): `offset` no es opcional; (2) **sin scroll-host** (AntD `target`, MDN: sticky se pega al ancestro con overflow más cercano AUNQUE no scrollee — el `root` del IO debe ser ese ancestro o la detección rompe en silencio); (3) "sin eventos v1" queda BAJO suelo (AntD `onChange(affixed)`) — recorte declarable, pero reservar el nombre del evento en el contrato ya; (4) borde bottom = centinela DISTINTO (`threshold:[1]`, por ratio) — es un centinela por borde con configs diferentes. - **Pitfalls**: `overflow:hidden` en cualquier ancestro mata el sticky en silencio (detector dev-mode con warning); IO recorta por CADA ancestro con overflow; `rootMargin` solo px/% (no em); frontera exacta no dispara (centinelas con altura real, no 1px); `data-stuck` llega un frame tarde (async — solo styling, nunca matemática de layout); Mantine Affix NO es comparable (es un slot fixed en Portal). #### anchor-nav (F1.6) - **Suelo convergente**: compensación de offset del heading (`targetOffset` ≠ offset del affix — AntD los separa; SIN él, el click-scroll entierra headings bajo nuestro propio sticky; par CSS: `scroll-margin-top` en targets); anidación con indent (CSS var de profundidad — Mantine `--depth-offset`, nosotros tokenizada); callback de cambio; **mecanismo de re-scan** (Bootstrap `refresh()`, Mantine `reinitializeRef` — nuestro ciclo de registro de partes lo cubre tipado, decirlo); **regla de zona muerta al fondo** (Docusaurus: sin anchor bajo el umbral → el ÚLTIMO activo; su regla de lectura: si el siguiente anchor está bajo la mitad superior → el ANTERIOR es el activo — modela "qué se está leyendo"). - **Política de hash a decidir** (silencio en el boceto): AntD migró a history API (`replace` opt-in existe porque el push del spy contamina el historial); spy → replace o nada, click → puede push; deep-link `#id` al cargar fija el activo inicial. - **aria-current: liderazgo barato** — solo 1 de 5 refs lo pone (Starlight `"true"`; Docusaurus/Bootstrap/Mantine/AntD = clase o data-attr). Valor spec-preciso: `"location"`; precedente shipped: `"true"`; **NUNCA `"page"`** (es para páginas de un set, no secciones). - Scroll programático barre el spy por secciones intermedias (flicker) → suprimir spy hasta asentar o closest-wins; targets ocultos (tabs/colapsados) se saltan y re-observan al mostrar. - Detección: **IO de banda** (Starlight: rootMargin calculado del chrome, recomputado en resize) — Mantine y Docusaurus usan scroll listeners crudos (la clase de bug que nuestra doctrina mata). #### empty-state (F1.2) y result (F1.3) — un esqueleto, dos componentes - Anatomía nuestra ≈ paridad exacta con shadcn `Empty`/Chakra v3. **Adoptar lo que nadie combina**: Atlaskit `headingLevel` (1–6, default 4) + `headingSize` DESACOPLADO (la única ref que hace bien la semántica de heading); `buttonGroupLabel` (nombre accesible del grupo de acciones); Polaris: UNA sola acción primaria (verbo+sustantivo), variantes secondary/tertiary; Chakra `size` sm/md/lg — **el tamaño compacto es lo que Table/Select/Command consumirán** (AntD SIMPLE preset + `ConfigProvider.renderEmpty` = inyección global en colecciones; los headless lo tratan como render-slot de colecciones: RAC `renderEmptyState` estampa `[data-empty]`); shadcn `EmptyMedia variant="icon"` → nuestro `data-media-kind`; clamp de ilustración (Atlaskit 160×160). - **result = solo de AntD** (nadie más lo tiene; Tailwind lo vende como páginas 404). Nuestro enum semántico > su `'404'` stringly-typed, PERO: (1) **falta `warning`** (7º valor de AntD; mapea natural a nuestro intent `risk`) — decidir explícito; (2) default `status` (AntD: `info`) — sin default es contract smell; (3) **AntD NO pinta 403/404/500 de rojo** (son situaciones, no fallos — ilustración neutra reemplaza al icono; nuestro media debe aceptar reemplazo total); (4) foco/anuncio tras navegación SPA (mover foco al título o live region polite — ninguna ref lo hace). #### callout (F1.4) - **Nuestros 4 intents NO cubren el vocabulario de facto (5)**: GitHub NOTE/TIP/IMPORTANT/WARNING/CAUTION (y Docusaurus separa note-gris de info-azul). Mapeo: neutral→NOTE · affirm→TIP · risk→WARNING · threat→CAUTION; **IMPORTANT (énfasis, púrpura) no tiene hueco**. Opciones: documentar la pérdida / `color` override sobre intent (modelo Radix: intent=semántica, color=pintura) / 5º variante. No fingir que 4 cubre 5. - **`role="alert"` estático = bug de shadcn, NO copiar** (MDN: alert solo anuncia en UPDATE — en carga no hace nada — y es assertive). Lo correcto: `role="note"` + `aria-labelledby`→title, con **escalación opt-in** (prop `live`/`role`) para callouts insertados dinámicamente (ahí sema puede sonar — cosa que ninguna ref puede expresar). - **El title NO es heading en ninguna ref** (GitHub: párrafo estilizado) — y un heading contaminaría el outline que ESCANEA nuestro propio anchor-nav (interacción real intra-suite). **El intent no viaja solo en color/icono**: GitHub lleva LABEL de texto visible — título default por intent = patrón robusto (color-blind + SR). - Radix como modelo de ejes: `variant soft|surface|outline` × color × size × highContrast, ortogonales. Nesting (Docusaurus): CSS tolerante a callout-en-callout. ### P4 — prose + sidebar — ✅ RECIBIDA 2026-07-21 (verificación a nivel de código fuente) **Método**: el agente leyó el `sidebar.tsx` real del registry de shadcn (new-york-v4) y el `styles.js`/`index.js` de @tailwindcss/typography en crudo — los datos de abajo son de código, no de prosa de docs. #### prose — suelo de paridad - **Lista de elementos de paridad** (unión tailwind+GitHub+Mantine): **h1–h6** (¡tailwind NO cubre h5/h6; GitHub y Mantine sí — cubrirlos!), `p`, `a` (+ `a code`, `a strong`), `strong/em` (con contextos: dentro de blockquote/th/h1–h4), `blockquote` (+ comillas de apertura/cierre en `p:first/last-of-type::before/::after`), `ul/ol/li` + `::marker` (counters/bullets tokenizados), correcciones de margen de listas anidadas y `p`-en-`li`, `dl/dt/dd`, `table` completa (thead/tbody/**tfoot**/th/td, trim de padding en primera/última celda), `code/pre` (+ reset `pre code`, backticks vía `code::before/::after`), `kbd` (doble box-shadow), `img` / `picture` (+ reset `picture > img`) / `video` / `figure` / `figcaption`, `hr`, `lead` (clase, no `:first-of-type`), y **recorte de bordes** `> :first-child { margin-top:0 }` / `> :last-child { margin-bottom:0 }` (composición limpia dentro de cards). **Tier GFM a decidir explícito**: task lists, footnotes, alerts, `details/summary`, `mark`, `sub/sup`. - **Medida**: 65ch embebido por tamaño + escape (`max-w-none` equivalente). **Tamaños**: tailwind re-deriva la escala COMPLETA 5 veces (sm 14 → 2xl 24) — nuestra fiche debe declarar qué eje (size/density) mapea esto. - **Overflow de tablas**: la respuesta sin wrapper es la de GitHub — `table { display:block; width:max-content; max-width:100%; overflow:auto }` (tailwind no tiene ninguna: las tablas desbordan la columna). - **El mecanismo `not-prose` (crítico)**: tailwind emite CADA selector como `:where(.prose el):not(:where([class~="not-prose"], [class~="not-prose"] *))` — especificidad CERO (cualquier clase gana) + exclusión "donut" del nodo y descendientes. Limitación admitida: no se puede re-anidar prose dentro del donut. Alternativa moderna: `@scope ([data-prose]) to ([data-prose-ignore])` (Baseline *newly available* desde finales de 2025 — evaluar contra nuestro suelo de soporte). - **Territorio sin reclamar** (superación): comillas de blockquote hardcodeadas en inglés en TODAS las refs (`\201C…`) → CSS `quotes` locale-aware vía langs; **exclusión automática de componentes embebidos** (todo lo que cuelgue de una raíz `data-{component}` se auto-excluye — las refs no pueden porque no poseen el markup de sus consumidores); dark gratis vía roles (NO copiar el patrón `-invert`: es un parche por colores horneados); code-in-heading (`h2 code` con corrección de tamaño — tailwind lo trae, Fumadocs lo presume). - **Avisos**: selectores de elemento = categoría "estructural bajo una parte" del eidos-lint — sancionarlo deliberadamente o el recipe lintará invalid; propiedades lógicas (`border-inline-start`) desde el día uno; anchors de heading son capa docs-shell (patrón a11y correcto: link DESPUÉS del heading, à la Starlight), no capa prose. #### sidebar — suelo de paridad - **shadcn = 23 partes + hook** (`Provider · Sidebar · Trigger · Rail · Inset · Input · Header · Footer · Separator · Content · Group · GroupLabel · GroupAction · GroupContent · Menu(ul) · MenuItem(li) · MenuButton · MenuAction · MenuBadge · MenuSkeleton · MenuSub(ul) · MenuSubItem · MenuSubButton(a) · useSidebar`). **Nuestro boceto tiene 8 — demasiado grueso**: `item` funde MenuItem (li posicionador) + MenuButton (interactivo, data-active, host del tooltip), y sin ese split no hay dónde colgar action/badge/skeleton/sub-nivel. shadcn-svelte lo porta 1:1 con dot-notation y Svelte 5 (`bind:open`, snippets para asChild) — el mapeo a nuestro idioma está probado. - **Dos ejes, no uno**: estado `data-state="expanded|collapsed"` + modo `collapsible="offcanvas|icon|none"` (shadcn estampa `data-collapsible` SOLO estando colapsado — ojo si espejamos nombres). Nuestro `data-rail` fusionaba ambos ejes → separar. `Rail` además es una PARTE física (franja de 16px toggle en el borde, tabIndex -1, cursores direccionales), no un estado. - **Props/constantes de referencia**: `side` left|right (table stakes), `variant` sidebar|floating|inset (diferible declarándolo), widths 16rem / 18rem móvil / 3rem icon como TOKENS (no consts TS), cookie `sidebar_state` 7d con lectura SSR → `defaultOpen` (evita flash de hidratación), atajo global Cmd/Ctrl+B con `preventDefault` (¡secuestra la negrita de cualquier editor embebido — el nuestro se scope-a fuera de contextos editables!), tooltips en modo icon montados SIEMPRE y suprimidos con `hidden` (no mount/unmount), móvil = Sheet <768px (nuestro Drawer con props label/description, no hacks sr-only). - **Exclusiones v1 legales SOLO con costuras**: `open`/`onOpenChange` controlado + `defaultOpen` + `toggle()` expuesto en el provider — la persistencia y el atajo viven en app-land únicamente si esas costuras existen desde v1; si no, es rediseño posterior. - **A11y — todas las refs suspenden**: shadcn NO tiene landmark `