{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "sidebar", "type": "registry:block", "title": "sidebar", "description": "Bloque Concorde sidebar — incluye sus 5 componentes.", "dependencies": [ "react" ], "registryDependencies": [], "files": [ { "path": "bloques/sidebar/desktop/dimensions.ts", "type": "registry:file", "target": "bloques/sidebar/desktop/dimensions.ts", "content": "/**\n * Dimensiones del bloque Sidebar — en un módulo PLANO (sin \"use client\") para\n * que los Server Components (catálogo /blocks, páginas de bloque) puedan importar\n * estas constantes. Importar valores desde un módulo \"use client\" hacia un Server\n * Component devuelve undefined → NaN en los cálculos de escala.\n */\n\nexport const SIDEBAR_WIDTH = 226;\nexport const SIDEBAR_HEIGHT = 1171;\nexport const SIDEBAR_COLLAPSED_WIDTH = 76; // solo iconos\n" }, { "path": "bloques/sidebar/desktop/Sidebar.tsx", "type": "registry:file", "target": "bloques/sidebar/desktop/Sidebar.tsx", "content": "\"use client\";\n\n/**\n * Sidebar — Bloque de navegación lateral (Voyager DS)\n * Fuente: Figma VOYAGER · \"SideBarDefault\" (226×1042)\n *\n * Estructura:\n * ┌──────────────────────────────┐ ← y=0\n * │ SidebarHeader (60px) │\n * ├──────────────────────────────┤ ← y=60 ← borde VYStrokes1\n * │ Nav primary items (4) │\n * │ ─── separator ─── │\n * │ Nav secondary items (1) │\n * │ │\n * │ ┌──────────────────────┐ │\n * │ │ SidebarBanner 200×264│ │\n * │ └──────────────────────┘ │\n * └──────────────────────────────┘ ← y=1042\n */\n\nimport { useState } from \"react\";\nimport type { JSX, ReactNode } from \"react\";\nimport SidebarHeader from \"./SidebarHeader\";\nimport SidebarBanner from \"./SidebarBanner\";\nimport SidebarItem, { SidebarSeparator, type SidebarNavItem } from \"./SidebarItem\";\nimport TodayIcon from \"@/concorde/components/TodayIcon\";\nimport OfferIcon from \"@/concorde/components/OfferIcon\";\nimport CategoryIcon from \"@/concorde/components/CategoryIcon\";\nimport BusinessIcon from \"@/concorde/components/BusinessIcon\";\nimport ServiceCenterIcon from \"@/concorde/components/ServiceCenterIcon\";\n\n// Dimensiones en un módulo plano (no-client) para que los Server Components puedan\n// importarlas sin obtener undefined. Se re-exportan para los consumidores cliente.\nexport { SIDEBAR_WIDTH, SIDEBAR_HEIGHT, SIDEBAR_COLLAPSED_WIDTH } from \"./dimensions\";\nimport { SIDEBAR_WIDTH, SIDEBAR_HEIGHT, SIDEBAR_COLLAPSED_WIDTH } from \"./dimensions\";\n\nconst EASE = \"cubic-bezier(0.4,0,0.2,1)\";\n\nconst DEFAULT_PRIMARY: SidebarNavItem[] = [\n { id: \"hoy\", icon: (s) => , label: \"Hoy\" },\n {\n id: \"oferta\", icon: (s) => , label: \"Tipo de oferta\", count: 2,\n subitems: [\n { id: \"en-vivo\", label: \"En Vivo\", count: 38 },\n { id: \"negociable\", label: \"Negociable\" },\n ],\n },\n { id: \"categoria\", icon: (s) => , label: \"Categoría\", count: 2 },\n { id: \"empresas\", icon: (s) => , label: \"Empresas\", count: 2 },\n];\n\nconst DEFAULT_SECONDARY: SidebarNavItem[] = [\n { id: \"centro\", icon: (s) => , label: \"Centro de Ayuda\" },\n];\n\nconst STYLE_ID = \"concorde-sidebar-styles\";\nconst STYLES = `\n.sb-root {\n transition: width 0.28s ${EASE}, height 0.28s ${EASE};\n}\n.sb-content-frame {\n border: 1.5px solid transparent;\n /* Borde VYStrokes1 — dirección exacta del SVG (paint20: vector 283.9,21.1 → 94deg) */\n background:\n linear-gradient(#2E0F70, #2E0F70) padding-box,\n linear-gradient(94deg, #FFFFFF 0%, #F4AC59 22.1%, #8460E5 74.5%, #FFFFFF 100%) border-box;\n transition: width 0.28s ${EASE};\n}\n.sb-banner {\n display: flex;\n justify-content: center;\n padding: 0 7px 4px;\n opacity: 1;\n max-height: 280px;\n overflow: hidden;\n transition: opacity 0.18s ease, max-height 0.28s ${EASE}, padding 0.28s ${EASE};\n}\n.sb-banner--collapsed {\n opacity: 0;\n max-height: 0;\n padding: 0;\n pointer-events: none;\n}\n@media (prefers-reduced-motion: reduce) {\n .sb-root, .sb-content-frame, .sb-banner { transition: none; }\n}\n`;\n\nlet _injected = false;\n\nexport type { SidebarNavItem };\n\nexport interface SidebarProps {\n logo?: ReactNode;\n primaryItems?: SidebarNavItem[];\n secondaryItems?: SidebarNavItem[];\n defaultActiveId?: string;\n /** Alto del sidebar. Por defecto SIDEBAR_HEIGHT; pásale el alto del canvas\n * cuando el contenido a la derecha sea más alto, para que crezca con él. */\n height?: number | string;\n /** Alto del bloque interno fijo (header + nav + banner). Por defecto\n * SIDEBAR_HEIGHT (igual que siempre: si `height` es mayor, el sobrante\n * queda como relleno morado plano debajo, sin estirar nav/banner).\n * Edítalo SOLO cuando el contenido de al lado sea más CORTO que el\n * sidebar — pásale ese mismo alto (igual a `height`) para que el spacer\n * interno se comprima y el sidebar no sobre por debajo del contenido. */\n contentHeight?: number;\n /** Alto de la cabecera fija (logo + hamburguesa), en px. Por defecto 60. Cuando\n * el contenido de al lado tiene su propio header y se escala, pásale la altura\n * de ESE header escalada (p.ej. HEADER_HEIGHT × scale) para que el top de los\n * iconos del sidebar quede alineado con el top del contenido. */\n headerHeight?: number;\n /** Colapsado (controlado). Si se omite, el Sidebar maneja su estado interno. */\n collapsed?: boolean;\n /** Callback al alternar el colapsado (para reaccionar desde el contenedor). */\n onCollapsedChange?: (collapsed: boolean) => void;\n onItemClick?: (id: string) => void;\n onBannerCta?: () => void;\n}\n\nexport default function Sidebar({\n logo,\n primaryItems = DEFAULT_PRIMARY,\n secondaryItems = DEFAULT_SECONDARY,\n defaultActiveId,\n height = SIDEBAR_HEIGHT,\n contentHeight = SIDEBAR_HEIGHT,\n headerHeight = 60,\n collapsed: collapsedProp,\n onCollapsedChange,\n onItemClick,\n onBannerCta,\n}: SidebarProps): JSX.Element {\n const [internalCollapsed, setInternalCollapsed] = useState(false);\n const collapsed = collapsedProp ?? internalCollapsed;\n const [activeId, setActiveId] = useState(defaultActiveId);\n\n function toggleCollapsed(): void {\n const next = !collapsed;\n if (collapsedProp === undefined) setInternalCollapsed(next);\n onCollapsedChange?.(next);\n }\n\n if (typeof document !== \"undefined\" && !_injected) {\n if (!document.getElementById(STYLE_ID)) {\n const el = document.createElement(\"style\");\n el.id = STYLE_ID;\n el.textContent = STYLES;\n document.head.appendChild(el);\n }\n _injected = true;\n }\n\n function handleItemClick(id: string) {\n setActiveId(id);\n onItemClick?.(id);\n }\n\n return (\n <>\n