From b79569c5e70cc731a437216f9dc893166bd5633c Mon Sep 17 00:00:00 2001 From: FrankZamora Date: Sat, 6 Dec 2025 19:14:09 -0600 Subject: [PATCH] docs: add templates-unificados openspec specification MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Defines unified listing templates architecture. Key additions: - Two new components: archive-header, post-grid - 10-phase implementation sequence - _page_visibility group pattern - Graceful handling of missing content - CSS pagination via CSSGeneratorService 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- openspec/specs/templates-unificados/spec.md | 417 ++++++++++++++++++++ 1 file changed, 417 insertions(+) create mode 100644 openspec/specs/templates-unificados/spec.md diff --git a/openspec/specs/templates-unificados/spec.md b/openspec/specs/templates-unificados/spec.md new file mode 100644 index 00000000..0ccd994e --- /dev/null +++ b/openspec/specs/templates-unificados/spec.md @@ -0,0 +1,417 @@ +# Especificacion de Templates Unificados para Blog/Archive + +## Purpose + +Define la arquitectura para unificar todos los templates de listados (blog, categorias, tags, archives) usando la misma estructura que `single.php`, aprovechando el sistema de visibilidad existente para controlar que componentes mostrar en cada contexto. Incluye la creacion de dos nuevos componentes: `archive-header` y `post-grid`. + +## Requirements + +### Requirement: Template Unificado para Listados + +All listing templates MUST use the same structure as single.php. + +#### Scenario: Estructura base de templates de listado +- **WHEN** se implementa home.php, archive.php, category.php o tag.php +- **THEN** DEBE usar la misma estructura que single.php +- **AND** DEBE llamar a roi_render_component() para cada componente +- **AND** la visibilidad se controla via PageVisibilityHelper::shouldShow() + +#### Scenario: Componentes que se llaman en templates de listado +- **WHEN** se renderiza un template de listado +- **THEN** DEBE llamar a roi_render_component('hero') +- **AND** DEBE llamar a roi_render_component('archive-header') +- **AND** DEBE llamar a roi_render_component('post-grid') +- **AND** DEBE llamar a roi_render_component('table-of-contents') en sidebar +- **AND** DEBE llamar a roi_render_component('cta-box-sidebar') en sidebar +- **AND** DEBE llamar a roi_render_component('contact-form') +- **AND** cada componente decide si renderiza segun show_on_archives + +#### Scenario: Determinacion de sidebar en listados +- **WHEN** se determina si mostrar sidebar en un listado +- **THEN** DEBE usar roi_should_render_any_wrapper(['table-of-contents', 'cta-box-sidebar']) +- **AND** si retorna true usar col-lg-9 para contenido principal +- **AND** si retorna false usar col-lg-12 para contenido principal + +#### Scenario: Paginacion en templates de listado +- **WHEN** se muestra paginacion en un listado +- **THEN** DEBE usar the_posts_pagination() de WordPress +- **AND** DEBE aplicar estilos Bootstrap via CSS del componente post-grid + +#### Scenario: CSS de paginacion generado por post-grid +- **WHEN** PostGridRenderer renderiza la paginacion +- **THEN** el CSS de paginacion DEBE generarse via CSSGeneratorService +- **AND** DEBE aplicar estilos Bootstrap (nav-links, page-numbers) +- **AND** los colores DEBEN ser configurables via grupo colors del schema + +--- + +### Requirement: Componente archive-header + +The archive-header component MUST display dynamic title and description for archive pages. + +#### Scenario: Ubicacion de archivos archive-header +- **WHEN** se crea el componente archive-header +- **THEN** schema DEBE estar en Schemas/archive-header.json +- **AND** Renderer DEBE estar en Public/ArchiveHeader/Infrastructure/Ui/ArchiveHeaderRenderer.php +- **AND** FormBuilder DEBE estar en Admin/ArchiveHeader/Infrastructure/Ui/ArchiveHeaderFormBuilder.php +- **AND** FieldMapper DEBE estar en Admin/ArchiveHeader/Infrastructure/FieldMapping/ArchiveHeaderFieldMapper.php + +#### Scenario: Namespaces PHP de archive-header +- **WHEN** se definen los namespaces para archive-header +- **THEN** Renderer DEBE usar namespace `ROITheme\Public\ArchiveHeader\Infrastructure\Ui` +- **AND** FormBuilder DEBE usar namespace `ROITheme\Admin\ArchiveHeader\Infrastructure\Ui` +- **AND** FieldMapper DEBE usar namespace `ROITheme\Admin\ArchiveHeader\Infrastructure\FieldMapping` + +#### Scenario: Deteccion automatica de tipo de archivo +- **WHEN** ArchiveHeaderRenderer detecta el tipo de pagina +- **THEN** para categoria DEBE mostrar "Categoria: [nombre]" +- **AND** para tag DEBE mostrar "Etiqueta: [nombre]" +- **AND** para autor DEBE mostrar "Articulos de: [nombre]" +- **AND** para fecha DEBE mostrar "Archivo: [Mes Ano]" +- **AND** para busqueda DEBE mostrar "Resultados para: [termino]" +- **AND** para blog home DEBE mostrar el valor de blog_title del schema + +#### Scenario: Grupos del schema archive-header +- **WHEN** se define el schema archive-header.json +- **THEN** DEBE incluir grupo visibility con priority 10 +- **AND** DEBE incluir grupo content con priority 20 +- **AND** DEBE incluir grupo typography con priority 30 +- **AND** DEBE incluir grupo colors con priority 40 +- **AND** DEBE incluir grupo spacing con priority 50 +- **AND** DEBE incluir grupo behavior con priority 70 +- **NOTE** archive-header NO incluye visual_effects (priority 60) porque es un componente de texto simple sin sombras, bordes redondeados ni transiciones hover + +#### Scenario: Campos obligatorios de visibility en archive-header +- **WHEN** se define grupo visibility en schema +- **THEN** DEBE incluir is_enabled como boolean con default true +- **AND** DEBE incluir show_on_desktop como boolean con default true +- **AND** DEBE incluir show_on_mobile como boolean con default true + +#### Scenario: Campos de _page_visibility en archive-header +- **WHEN** se configura visibilidad por tipo de pagina en FieldMapper +- **THEN** DEBE mapear campo show_on_home en grupo _page_visibility con default false +- **AND** DEBE mapear campo show_on_posts en grupo _page_visibility con default false +- **AND** DEBE mapear campo show_on_pages en grupo _page_visibility con default false +- **AND** DEBE mapear campo show_on_archives en grupo _page_visibility con default true +- **AND** DEBE mapear campo show_on_search en grupo _page_visibility con default false +- **NOTE** Los campos _page_visibility NO van en el schema JSON, se manejan via FieldMapper y VisibilityDefaults +- **NOTE** show_on_archives en true porque este componente solo tiene sentido en archives + +#### Scenario: Campos de content en archive-header +- **WHEN** se define grupo content +- **THEN** DEBE incluir blog_title como text con default "Blog" +- **AND** DEBE incluir show_post_count como boolean con default true +- **AND** DEBE incluir show_description como boolean con default true + +#### Scenario: Campos de typography en archive-header +- **WHEN** se define grupo typography +- **THEN** DEBE incluir heading_level como select con options ["h1", "h2", "h3", "h4", "h5", "h6"] y default "h1" +- **AND** DEBE incluir title_size como text con default "2rem" +- **AND** DEBE incluir title_weight como text con default "700" +- **AND** DEBE incluir description_size como text con default "1rem" + +#### Scenario: Campos de colors en archive-header +- **WHEN** se define grupo colors +- **THEN** DEBE incluir title_color como color con default "#0E2337" +- **AND** DEBE incluir description_color como color con default "#6b7280" +- **AND** DEBE incluir count_bg_color como color con default "#FF8600" +- **AND** DEBE incluir count_text_color como color con default "#ffffff" + +#### Scenario: Campos de spacing en archive-header +- **WHEN** se define grupo spacing +- **THEN** DEBE incluir margin_top como text con default "2rem" +- **AND** DEBE incluir margin_bottom como text con default "2rem" +- **AND** DEBE incluir padding como text con default "1.5rem" + +#### Scenario: Campos de behavior en archive-header +- **WHEN** se define grupo behavior +- **THEN** DEBE incluir is_sticky como boolean con default false +- **AND** DEBE incluir sticky_offset como text con default "0" + +--- + +### Requirement: Componente post-grid + +The post-grid component MUST display posts from the main WordPress loop in a grid layout. + +#### Scenario: Ubicacion de archivos post-grid +- **WHEN** se crea el componente post-grid +- **THEN** schema DEBE estar en Schemas/post-grid.json +- **AND** Renderer DEBE estar en Public/PostGrid/Infrastructure/Ui/PostGridRenderer.php +- **AND** FormBuilder DEBE estar en Admin/PostGrid/Infrastructure/Ui/PostGridFormBuilder.php +- **AND** FieldMapper DEBE estar en Admin/PostGrid/Infrastructure/FieldMapping/PostGridFieldMapper.php + +#### Scenario: Namespaces PHP de post-grid +- **WHEN** se definen los namespaces para post-grid +- **THEN** Renderer DEBE usar namespace `ROITheme\Public\PostGrid\Infrastructure\Ui` +- **AND** FormBuilder DEBE usar namespace `ROITheme\Admin\PostGrid\Infrastructure\Ui` +- **AND** FieldMapper DEBE usar namespace `ROITheme\Admin\PostGrid\Infrastructure\FieldMapping` + +#### Scenario: Diferencia entre post-grid y related-post +- **WHEN** PostGridRenderer obtiene los posts +- **THEN** DEBE usar global $wp_query para obtener posts del loop principal +- **AND** NO DEBE crear su propio WP_Query como hace RelatedPostRenderer +- **AND** DEBE llamar wp_reset_postdata() al finalizar si modifica el loop + +#### Scenario: Grupos del schema post-grid +- **WHEN** se define el schema post-grid.json +- **THEN** DEBE incluir grupo visibility con priority 10 +- **AND** DEBE incluir grupo content con priority 20 +- **AND** DEBE incluir grupo typography con priority 30 +- **AND** DEBE incluir grupo colors con priority 40 +- **AND** DEBE incluir grupo spacing con priority 50 +- **AND** DEBE incluir grupo visual_effects con priority 60 +- **AND** DEBE incluir grupo layout con priority 80 +- **AND** DEBE incluir grupo media con priority 90 + +#### Scenario: Campos obligatorios de visibility en post-grid +- **WHEN** se define grupo visibility en schema +- **THEN** DEBE incluir is_enabled como boolean con default true +- **AND** DEBE incluir show_on_desktop como boolean con default true +- **AND** DEBE incluir show_on_mobile como boolean con default true + +#### Scenario: Campos de _page_visibility en post-grid +- **WHEN** se configura visibilidad por tipo de pagina en FieldMapper +- **THEN** DEBE mapear campo show_on_home en grupo _page_visibility con default true +- **AND** DEBE mapear campo show_on_posts en grupo _page_visibility con default false +- **AND** DEBE mapear campo show_on_pages en grupo _page_visibility con default false +- **AND** DEBE mapear campo show_on_archives en grupo _page_visibility con default true +- **AND** DEBE mapear campo show_on_search en grupo _page_visibility con default true +- **NOTE** Los campos _page_visibility NO van en el schema JSON, se manejan via FieldMapper y VisibilityDefaults +- **NOTE** show_on_home en true para mostrar grid en pagina de blog principal +- **NOTE** show_on_archives en true porque este componente es para listados +- **NOTE** show_on_search en true para mostrar resultados de busqueda + +#### Scenario: Campos de content en post-grid +- **WHEN** se define grupo content +- **THEN** DEBE incluir show_thumbnail como boolean con default true +- **AND** DEBE incluir show_excerpt como boolean con default true +- **AND** DEBE incluir show_meta como boolean con default true +- **AND** DEBE incluir show_categories como boolean con default true +- **AND** DEBE incluir excerpt_length como select con options ["10", "15", "20", "25", "30"] y default "20" +- **AND** DEBE incluir read_more_text como text con default "Leer mas" +- **AND** DEBE incluir no_posts_message como text con default "No se encontraron publicaciones" + +#### Scenario: Campos de media en post-grid +- **WHEN** se define grupo media +- **THEN** DEBE incluir fallback_image como url con default "" +- **AND** DEBE incluir fallback_image_alt como text con default "Imagen por defecto" +- **AND** fallback_image_alt es obligatorio para accesibilidad WCAG + +#### Scenario: Campos de typography en post-grid +- **WHEN** se define grupo typography +- **THEN** DEBE incluir heading_level como select con options ["h2", "h3", "h4", "h5", "h6"] y default "h3" +- **AND** DEBE incluir card_title_size como text con default "1.1rem" +- **AND** DEBE incluir card_title_weight como text con default "600" +- **AND** DEBE incluir excerpt_size como text con default "0.9rem" +- **AND** DEBE incluir meta_size como text con default "0.8rem" + +#### Scenario: Campos de colors en post-grid +- **WHEN** se define grupo colors +- **THEN** DEBE incluir card_bg_color como color con default "#ffffff" +- **AND** DEBE incluir card_title_color como color con default "#0E2337" +- **AND** DEBE incluir card_hover_bg_color como color con default "#f9fafb" +- **AND** DEBE incluir card_border_color como color con default "#e5e7eb" +- **AND** DEBE incluir card_hover_border_color como color con default "#FF8600" +- **AND** DEBE incluir excerpt_color como color con default "#6b7280" +- **AND** DEBE incluir meta_color como color con default "#9ca3af" +- **AND** DEBE incluir category_bg_color como color con default "#FFF5EB" +- **AND** DEBE incluir category_text_color como color con default "#FF8600" + +#### Scenario: Campos de spacing en post-grid +- **WHEN** se define grupo spacing +- **THEN** DEBE incluir grid_gap como text con default "1.5rem" +- **AND** DEBE incluir card_padding como text con default "1.25rem" +- **AND** DEBE incluir section_margin_top como text con default "0" +- **AND** DEBE incluir section_margin_bottom como text con default "2rem" + +#### Scenario: Campos de visual_effects en post-grid +- **WHEN** se define grupo visual_effects +- **THEN** DEBE incluir card_border_radius como text con default "0.5rem" +- **AND** DEBE incluir card_shadow como text con default "0 1px 3px rgba(0,0,0,0.1)" +- **AND** DEBE incluir card_hover_shadow como text con default "0 4px 12px rgba(0,0,0,0.15)" +- **AND** DEBE incluir card_transition como text con default "all 0.3s ease" +- **AND** DEBE incluir image_border_radius como text con default "0.375rem" + +#### Scenario: Campos de layout en post-grid +- **WHEN** se define grupo layout +- **THEN** DEBE incluir columns_desktop como select con options ["2", "3", "4"] y default "3" +- **AND** DEBE incluir columns_tablet como select con options ["1", "2", "3"] y default "2" +- **AND** DEBE incluir columns_mobile como select con options ["1", "2"] y default "1" +- **AND** DEBE incluir image_position como select con options ["top", "left", "none"] y default "top" + +--- + +### Requirement: Manejo Graceful de Contenido Faltante + +The post-grid component MUST handle missing content gracefully. + +#### Scenario: Post sin imagen destacada +- **WHEN** un post no tiene thumbnail y show_thumbnail es true +- **THEN** si fallback_image tiene valor DEBE mostrar esa imagen con fallback_image_alt +- **AND** si fallback_image esta vacio DEBE omitir la imagen sin romper el layout +- **AND** NO DEBE mostrar imagen rota o placeholder generico + +#### Scenario: Post sin excerpt +- **WHEN** un post no tiene excerpt y show_excerpt es true +- **THEN** DEBE generar excerpt automatico desde post_content +- **AND** DEBE respetar excerpt_length del schema +- **AND** DEBE usar wp_trim_words() para truncar + +#### Scenario: Post sin categorias +- **WHEN** un post no tiene categorias y show_categories es true +- **THEN** DEBE omitir la seccion de categorias +- **AND** NO DEBE mostrar "Sin categoria" u otro texto placeholder + +#### Scenario: No posts found - Query vacia +- **WHEN** have_posts() retorna false en un template de listado +- **THEN** post-grid DEBE mostrar mensaje configurable de "no hay posts" +- **AND** el mensaje DEBE usar campo no_posts_message del schema con default "No se encontraron publicaciones" +- **AND** DEBE aplicar estilos consistentes con el design system +- **AND** NO DEBE romper el layout de la pagina + +--- + +### Requirement: Visibilidad por Tipo de Pagina + +Components MUST respect the show_on_archives setting in _page_visibility group. + +#### Scenario: Patron de visibilidad por tipo de pagina +- **WHEN** se implementa visibilidad por tipo de pagina +- **THEN** los campos show_on_home, show_on_posts, show_on_pages, show_on_archives, show_on_search +- **AND** DEBEN estar en grupo _page_visibility (NO en visibility) +- **AND** DEBEN mapearse via FieldMapper del componente +- **AND** DEBEN evaluarse via PageVisibilityHelper::shouldShow() + +#### Scenario: Configuracion por defecto de show_on_archives para nuevos componentes +- **WHEN** se configura _page_visibility para componentes nuevos +- **THEN** archive-header DEBE tener show_on_archives true en _page_visibility +- **AND** post-grid DEBE tener show_on_archives true en _page_visibility + +#### Scenario: Componentes existentes en archives +- **WHEN** se evalua que componentes mostrar en archives via _page_visibility +- **THEN** hero DEBE tener show_on_archives false por defecto (configurable) +- **AND** table-of-contents DEBE tener show_on_archives false +- **AND** featured-image DEBE tener show_on_archives false +- **AND** social-share DEBE tener show_on_archives false +- **AND** related-post DEBE tener show_on_archives false +- **AND** cta-box-sidebar DEBE tener show_on_archives true +- **AND** contact-form DEBE tener show_on_archives configurable + +#### Scenario: Llamada a componente con visibilidad deshabilitada (Patron Template Unificado) +- **GIVEN** el template unificado llama a TODOS los componentes para mantener consistencia +- **WHEN** un template llama roi_render_component() para un componente +- **AND** ese componente tiene show_on_archives false +- **THEN** el componente NO DEBE renderizarse (retorna string vacio) +- **AND** esto es comportamiento correcto y esperado, NO un error +- **AND** permite que el admin habilite/deshabilite componentes sin modificar templates +- **NOTE** Por ejemplo: table-of-contents se llama en sidebar pero no renderiza en archives porque show_on_archives=false + +--- + +### Requirement: Templates a Modernizar + +These templates MUST be updated to use the unified structure. + +#### Scenario: Modernizar home.php +- **WHEN** se actualiza home.php +- **THEN** DEBE reemplazar get_template_part() con roi_render_component() +- **AND** DEBE eliminar referencia a TemplateParts/content.php +- **AND** DEBE usar estructura unificada con hero, archive-header, post-grid + +#### Scenario: Modernizar archive.php +- **WHEN** se actualiza archive.php +- **THEN** DEBE reemplazar get_template_part() con roi_render_component() +- **AND** DEBE eliminar referencia a TemplateParts/content.php +- **AND** DEBE usar estructura unificada + +#### Scenario: Modernizar category.php +- **GIVEN** category.php existe en roi-theme/ (verificado) +- **WHEN** se actualiza category.php +- **THEN** DEBE reemplazar get_template_part() con roi_render_component() +- **AND** DEBE eliminar referencia a TemplateParts/content.php +- **AND** DEBE usar estructura unificada + +#### Scenario: Modernizar tag.php +- **GIVEN** tag.php existe en roi-theme/ (verificado) +- **WHEN** se actualiza tag.php +- **THEN** DEBE reemplazar get_template_part() con roi_render_component() +- **AND** DEBE eliminar referencia a TemplateParts/content.php +- **AND** DEBE usar estructura unificada + +#### Scenario: Modernizar author.php +- **GIVEN** author.php existe en roi-theme/ (verificado) +- **WHEN** se actualiza author.php +- **THEN** DEBE reemplazar get_template_part() con roi_render_component() +- **AND** DEBE eliminar referencia a TemplateParts/content.php +- **AND** DEBE usar estructura unificada +- **AND** archive-header detectara automaticamente contexto de autor + +#### Scenario: Modernizar date.php +- **GIVEN** date.php existe en roi-theme/ (verificado) +- **WHEN** se actualiza date.php +- **THEN** DEBE reemplazar get_template_part() con roi_render_component() +- **AND** DEBE eliminar referencia a TemplateParts/content.php +- **AND** DEBE usar estructura unificada +- **AND** archive-header detectara automaticamente contexto de fecha + +#### Scenario: Modernizar search.php +- **GIVEN** search.php existe en roi-theme/ (verificado) +- **WHEN** se actualiza search.php +- **THEN** DEBE reemplazar get_template_part() con roi_render_component() +- **AND** DEBE eliminar referencia a TemplateParts/content.php +- **AND** DEBE usar estructura unificada con post-grid +- **AND** archive-header detectara automaticamente contexto de busqueda mostrando "Resultados: [termino]" + +--- + +### Requirement: Orden de Implementacion + +Components and templates MUST be implemented in a specific order. + +#### Scenario: Secuencia de implementacion +- **WHEN** se implementa esta especificacion +- **THEN** Fase 1 es crear componente archive-header (5 pasos del flujo) +- **AND** Fase 2 es crear componente post-grid (5 pasos del flujo) +- **AND** Fase 3 es modernizar home.php +- **AND** Fase 4 es modernizar archive.php +- **AND** Fase 5 es modernizar category.php +- **AND** Fase 6 es modernizar tag.php +- **AND** Fase 7 es modernizar author.php +- **AND** Fase 8 es modernizar date.php +- **AND** Fase 9 es modernizar search.php +- **AND** Fase 10 es configurar visibilidad de componentes existentes + +#### Scenario: Cada componente sigue flujo de 5 fases +- **WHEN** se crea archive-header o post-grid +- **THEN** DEBE seguir Fase 1: Schema JSON +- **AND** DEBE seguir Fase 2: Sincronizacion wp roi-theme sync-component +- **AND** DEBE seguir Fase 3: Renderer +- **AND** DEBE seguir Fase 4: FormBuilder +- **AND** DEBE seguir Fase 5: Validacion validate-architecture.php + +--- + +### Requirement: Dependencias Existentes + +The implementation MUST use existing infrastructure. + +#### Scenario: Uso de PageVisibilityHelper +- **WHEN** un Renderer verifica visibilidad +- **THEN** DEBE usar PageVisibilityHelper::shouldShow(componentName) +- **AND** esta en Shared/Infrastructure/Services/PageVisibilityHelper.php + +#### Scenario: Uso de CSSGeneratorInterface +- **WHEN** un Renderer genera CSS +- **THEN** DEBE inyectar CSSGeneratorInterface via constructor +- **AND** DEBE usar $this->cssGenerator->generate() + +#### Scenario: Uso de roi_should_render_any_wrapper +- **WHEN** un template determina si mostrar sidebar +- **THEN** DEBE usar roi_should_render_any_wrapper() +- **AND** esta definida en functions-addon.php linea 423 + +#### Scenario: Uso de DIContainer +- **WHEN** se instancian servicios +- **THEN** DEBE usar DIContainer::getInstance() +- **AND** NO DEBE instanciar servicios con new directamente