{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "banners", "type": "registry:block", "title": "banners", "description": "Bloque Concorde banners — incluye sus 3 componentes.", "dependencies": [ "react" ], "registryDependencies": [], "files": [ { "path": "bloques/banners/desktop/AssetBanner.tsx", "type": "registry:file", "target": "bloques/banners/desktop/AssetBanner.tsx", "content": "/**\n * AssetBanner — 766×192 · banner que usa el ASSET REAL de producción como\n * fondo (personaje + decoración + pill + \"Ofertas\" ya vienen en el PNG) y solo\n * sobrepone lo dinámico: el número de ofertas y, en categoría, el chip de ruta.\n *\n * Assets en public/banners/ (descargados de vmcsubastas.com, 1532×384 = 2×).\n * Usa el alto NATURAL del asset (192 a ancho 766) para no recortar la derecha\n * (donde vive «Ofertas»). Estático, sin efectos.\n */\n\nimport type { JSX } from \"react\";\nimport { CarIcon } from \"./decor\";\nimport { BANNER_WIDTH, BANNER_HEIGHT } from \"./dimensions\";\n\nexport const ASSET_WIDTH = BANNER_WIDTH; // 766\nexport const ASSET_HEIGHT = BANNER_HEIGHT; // 192 — alto natural del asset\n\nexport type AssetBannerKind = \"en-vivo\" | \"negociable\" | \"categoria\";\n\nexport interface AssetBannerProps {\n kind: AssetBannerKind;\n /** Número de ofertas — se sobrepone a la derecha de «Ofertas» del asset */\n count: number | string;\n /** Chip de ruta (solo categoría) */\n chip?: { label: string; icon?: \"car\" };\n className?: string;\n}\n\nconst ASSET: Record = {\n \"en-vivo\": \"/banners/en_vivo.png\",\n \"negociable\": \"/banners/negociable.png\",\n \"categoria\": \"/banners/categoria.png\",\n};\n\nexport default function AssetBanner({ kind, count, chip, className = \"\" }: AssetBannerProps): JSX.Element {\n return (\n \n {/* Asset real de producción como fondo (ancho completo, alto natural) */}\n \n\n {/* Número de ofertas — a la derecha de «Ofertas» del asset\n («Ofertas» termina ~x628; el número va desde ahí, centrado en y≈96) */}\n \n {count}\n \n\n {/* Chip de ruta (solo categoría) — esquina inferior derecha */}\n {chip && (\n \n {chip.label}\n {chip.icon === \"car\" && }\n \n )}\n \n );\n}\n" }, { "path": "bloques/banners/desktop/decor.tsx", "type": "registry:file", "target": "bloques/banners/desktop/decor.tsx", "content": "/**\n * decor — piezas decorativas estáticas de los banners 766×272 (sin efectos).\n * Recrean los elementos de los banners de producción: monedas SubasCoin\n * (>S<), sparkles de 4 puntas, rombos y el chevrón \"V\" gigante del fondo.\n */\n\nimport type { CSSProperties, JSX } from \"react\";\n\nconst ABS = (style?: CSSProperties): CSSProperties => ({ position: \"absolute\", pointerEvents: \"none\", ...style });\n\n// ─── Moneda SubasCoin ─────────────────────────────────────────────────────────\n\nexport interface CoinProps {\n size?: number;\n /** filled = blanca con glifo morado · outline = solo contorno blanco */\n variant?: \"filled\" | \"outline\";\n style?: CSSProperties;\n}\n\nexport function Coin({ size = 44, variant = \"filled\", style }: CoinProps): JSX.Element {\n const filled = variant === \"filled\";\n const ink = filled ? \"#3B1782\" : \"#FFFFFF\";\n return (\n \n \n \n S\n \n \n \n );\n}\n\n// ─── Sparkle de 4 puntas ──────────────────────────────────────────────────────\n\nexport interface SparkleProps {\n size?: number;\n color?: string;\n /** true = solo contorno */\n outline?: boolean;\n style?: CSSProperties;\n}\n\nexport function Sparkle({ size = 16, color = \"#E8503A\", outline = false, style }: SparkleProps): JSX.Element {\n return (\n \n \n \n );\n}\n\n// ─── Rombo pequeño ────────────────────────────────────────────────────────────\n\nexport function Diamond({ size = 10, color = \"#3B1782\", style }: { size?: number; color?: string; style?: CSSProperties }): JSX.Element {\n return (\n \n \n \n );\n}\n\n// ─── Chevrón \"V\" gigante del fondo (doble trazo, como el logo) ───────────────\n\nexport interface ChevronVProps {\n width?: number;\n color?: string;\n strokeWidth?: number;\n style?: CSSProperties;\n}\n\nexport function ChevronV({ width = 240, color = \"rgba(59,23,130,0.55)\", strokeWidth = 3, style }: ChevronVProps): JSX.Element {\n const height = width * 0.75;\n return (\n \n \n \n \n );\n}\n\n// ─── SubasCoin grande (gema del OfferCard, escalable) ─────────────────────────\n// IDs de gradiente fijos: si hay varias instancias, los defs colisionan pero son\n// idénticos, así que el render es el mismo.\n\nexport function BigGem({ size = 96, style }: { size?: number; style?: CSSProperties }): JSX.Element {\n const h = (size * 30) / 28;\n return (\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n );\n}\n\n// ─── Ribbons — curvas de luz iridiscentes (estilo voyager-slides, estáticas) ──\n// Trazos bezier con el trío del sistema \"ganador\" (#F4AC59 → #8460E5 → #ffffff),\n// desenfocados y en mix-blend screen sobre fondos oscuros.\n\nexport interface RibbonsProps {\n /** Color medio de las cintas (default lila #8460E5) */\n mid?: string;\n /** Color cálido/acento (default ámbar #F4AC59) */\n warm?: string;\n style?: CSSProperties;\n}\n\nexport function Ribbons({ mid = \"#8460E5\", warm = \"#F4AC59\", style }: RibbonsProps): JSX.Element {\n const gid = `rib-${warm.slice(1)}-${mid.slice(1)}`;\n return (\n
\n {/* capa difusa (luz) */}\n \n \n \n \n \n \n \n \n \n \n \n \n {/* capa nítida (filo de la cinta) */}\n \n \n \n \n
\n );\n}\n\n// ─── GlassWaves — ondas de vidrio en capas (superficies onduladas translúcidas) ──\n// No son luces radiales: son pliegues de \"vidrio\" que refractan el fondo del tono.\n// Bandas onduladas con relleno blanco translúcido + blur, borde claro fino (filo del\n// vidrio) y un tinte del color medio. Sin morado vault de fondo.\n\nexport interface GlassWavesProps {\n /** Color de tinte de las ondas (medio del tono) */\n tint?: string;\n style?: CSSProperties;\n}\n\nexport function GlassWaves({ tint = \"#ffffff\", style }: GlassWavesProps): JSX.Element {\n return (\n
\n {/* Onda 1 — banda ancha diagonal, vidrio esmerilado */}\n \n \n \n \n {/* Onda 2 — pliegue superior fino con filo brillante */}\n \n \n \n \n {/* Onda 3 — pliegue inferior, sombra suave del vidrio */}\n \n \n \n \n {/* Highlight especular puntual (brillo del vidrio) */}\n
\n
\n );\n}\n\n// ─── Gema SubasCoin en OUTLINE (solo bordes, sin relleno) ─────────────────────\n// Misma geometría que la gema del OfferCard pero como forma decorativa grande\n// de fondo: trazos blancos translúcidos, sin fill.\n\nexport function GemOutline({ size = 220, color = \"rgba(255,255,255,0.30)\", style }: { size?: number; color?: string; style?: CSSProperties }): JSX.Element {\n const h = (size * 30) / 28;\n // strokeWidth en unidades del viewBox (28 de ancho): 0.55 ≈ 4px a 200px\n return (\n \n \n \n \n \n \n );\n}\n\n// ─── Iconos de pill / chip ────────────────────────────────────────────────────\n\n/** Apretón de manos (pill EN VIVO / NEGOCIABLE) — glifo limpio estilo Lucide */\nexport function HandshakeIcon({ size = 18, color = \"#FFFFFF\" }: { size?: number; color?: string }): JSX.Element {\n return (\n \n \n \n \n \n \n \n );\n}\n\n/** Carro (chip de categoría Vehicular) — path del CategoryCard vehicular */\nexport function CarIcon({ size = 22, color = \"#FFFFFF\" }: { size?: number; color?: string }): JSX.Element {\n return (\n \n \n \n );\n}\n" }, { "path": "bloques/banners/desktop/dimensions.ts", "type": "registry:file", "target": "bloques/banners/desktop/dimensions.ts", "content": "/**\n * Dimensiones de los banners de navegación — módulo plano sin \"use client\".\n * Formato único 766×192 — el alto NATURAL de los assets de producción\n * (1532×384 a escala ×0.5). Todas las variantes comparten este alto.\n */\n\nexport const BANNER_WIDTH = 766;\nexport const BANNER_HEIGHT = 192;\n" }, { "path": "bloques/banners/desktop/EmpresaBanner.tsx", "type": "registry:file", "target": "bloques/banners/desktop/EmpresaBanner.tsx", "content": "/**\n * EmpresaBanner — 766×192 · banner de página de empresa/vendedor.\n * Usa el ASSET REAL de producción (public/banners/empresa.png, 1532×384 = 2×:\n * personaje + círculo del logo + cajas Ventas/Participantes vacías) y sobrepone\n * el texto en las coordenadas del asset. Estático, sin efectos.\n *\n * Nota: usa el alto NATURAL del asset (192, no 272) para que las coordenadas\n * del PNG coincidan sin recortar la parte derecha (cajas de stats).\n */\n\nimport type { JSX } from \"react\";\nimport { BANNER_WIDTH, BANNER_HEIGHT } from \"./dimensions\";\n\nexport const EMPRESA_WIDTH = BANNER_WIDTH; // 766\nexport const EMPRESA_HEIGHT = BANNER_HEIGHT; // 192 — alto natural del asset\n\nexport interface EmpresaBannerProps {\n nombre: string;\n /** Texto dentro del círculo morado del logo (default: nombre) */\n logoText?: string;\n rating: string;\n ratingLabel: string;\n opiniones: string;\n descripcion: string;\n ventas: string;\n participantes: string;\n className?: string;\n}\n\nfunction Star(): JSX.Element {\n return (\n \n \n \n );\n}\n\nexport default function EmpresaBanner({\n nombre,\n logoText,\n rating,\n ratingLabel,\n opiniones,\n descripcion,\n ventas,\n participantes,\n className = \"\",\n}: EmpresaBannerProps): JSX.Element {\n return (\n \n {/* Asset real de producción como fondo (ancho completo, alto natural) */}\n \n\n {/* Logo dentro del círculo morado (asset ø≈110 centrado en x≈182 / y≈60) */}\n \n {logoText ?? nombre}\n \n\n {/* Nombre + rating + descripción (zona blanca central del asset) */}\n
\n

{nombre}

\n
\n {rating}\n \n {ratingLabel}\n ·\n {opiniones}\n
\n

\n {descripcion}\n

\n
\n\n {/* Valores de stats — dentro de las cajas del asset\n (Ventas centro y≈65, Participantes centro y≈138 · x centro ≈ 680) */}\n \n {ventas}\n \n \n {participantes}\n \n
\n );\n}\n" }, { "path": "bloques/banners/desktop/EmpresaBannerAlt.tsx", "type": "registry:file", "target": "bloques/banners/desktop/EmpresaBannerAlt.tsx", "content": "/**\n * EmpresaBannerAlt — 766×192 · variantes MODERNAS del banner de empresa,\n * sin ilustraciones. Usan el design system actual de los componentes:\n * · rating con StarIcon (la estrella real del DS)\n * · logo circular con gradiente morado de marca (header de DetailCard)\n * · cajas de stats con borde gradiente lila estilo CategoryCard\n * · icono BusinessIcon del sidebar como marca de sección\n * Estático, sin efectos.\n */\n\nimport type { CSSProperties, JSX } from \"react\";\nimport StarIcon from \"@/concorde/components/StarIcon\";\nimport BusinessIcon from \"@/concorde/components/BusinessIcon\";\nimport { BANNER_WIDTH, BANNER_HEIGHT } from \"./dimensions\";\n\nexport { BANNER_WIDTH, BANNER_HEIGHT } from \"./dimensions\";\n\nexport type EmpresaAltLayout = \"logo-left\" | \"stats-bottom\" | \"panel\" | \"photo\" | \"glass\" | \"dark-hero\" | \"split\" | \"rating-hero\";\n\nexport interface EmpresaBannerAltProps {\n nombre: string;\n logoText?: string;\n rating: string;\n ratingLabel: string;\n opiniones: string;\n descripcion: string;\n ventas: string;\n participantes: string;\n layout: EmpresaAltLayout;\n className?: string;\n}\n\n/** Borde gradiente lila estilo CategoryCard (fondo claro) */\nconst GRADIENT_BORDER: CSSProperties = {\n border: \"1px solid transparent\",\n backgroundImage:\n \"linear-gradient(160deg, #ffffff 0%, #f4f5f9 100%), linear-gradient(135deg, #9c96f8 0%, rgba(255,255,255,0.65) 38%, #7364de 70%, #9c96f8 100%)\",\n backgroundOrigin: \"padding-box, border-box\",\n backgroundClip: \"padding-box, border-box\",\n};\n\nfunction LogoCircle({ text, size = 96 }: { text: string; size?: number }): JSX.Element {\n return (\n \n {text}\n \n );\n}\n\n/** Logo circular blanco (para fondos oscuros) */\nfunction LogoCircleDark({ text, size = 88 }: { text: string; size?: number }): JSX.Element {\n return (\n \n {text}\n \n );\n}\n\nfunction Rating({ rating, ratingLabel, opiniones }: { rating: string; ratingLabel: string; opiniones: string }): JSX.Element {\n return (\n
\n {rating}\n \n {ratingLabel}\n ·\n {opiniones}\n
\n );\n}\n\n/** Stat con los ESTILOS del StatPill del bloque Sala (dark glass + VYStrokes) */\nfunction GlassStat({ label, value }: { label: string; value: string }): JSX.Element {\n return (\n \n {label}\n {value}\n \n );\n}\n\nfunction StatCard({ label, value }: { label: string; value: string }): JSX.Element {\n return (\n
\n \n {label}\n \n \n {value}\n
\n \n );\n}\n\nconst shell: CSSProperties = {\n position: \"relative\",\n width: BANNER_WIDTH,\n height: BANNER_HEIGHT,\n background: \"linear-gradient(160deg, #ffffff 0%, #f4f5f9 100%)\",\n overflow: \"hidden\",\n fontFamily: 'var(--vmc-font-display, \"Plus Jakarta Sans\", -apple-system, sans-serif)',\n flexShrink: 0,\n boxSizing: \"border-box\",\n};\n\nexport default function EmpresaBannerAlt({\n nombre,\n logoText,\n rating,\n ratingLabel,\n opiniones,\n descripcion,\n ventas,\n participantes,\n layout,\n className = \"\",\n}: EmpresaBannerAltProps): JSX.Element {\n const logo = logoText ?? nombre;\n\n // ── \"split\" — mitad clara (datos) | mitad morada diagonal (stats con glow) ──\n if (layout === \"split\") {\n return (\n
\n {/* mitad derecha morada con corte diagonal */}\n \n {/* luces del lado morado */}\n
\n
\n {/* datos en la mitad clara */}\n
\n \n
\n

{nombre}

\n
\n

{descripcion}

\n
\n
\n {/* stats en la mitad morada */}\n
\n
\n Ventas\n {ventas}\n
\n \n
\n Particip.\n {participantes}\n
\n
\n
\n );\n }\n\n // ── \"rating-hero\" — el rating como protagonista con glow dorado ──\n if (layout === \"rating-hero\") {\n return (\n \n {/* luces */}\n
\n
\n
\n
\n
\n \n
\n

{nombre}

\n

{descripcion}

\n
\n Ventas {ventas}\n Participantes {participantes}\n
\n
\n {/* rating protagonista */}\n
\n
\n {rating}\n \n
\n \n {ratingLabel} · {opiniones}\n \n
\n
\n );\n }\n\n // ── \"glass\" — panel glass centrado sobre gradiente morado→naranja ──\n if (layout === \"glass\") {\n return (\n \n {/* luces de fondo */}\n
\n
\n
\n
\n
\n {/* panel glass con todo el contenido */}\n \n \n {logo}\n
\n
\n

{nombre}

\n
\n {rating}\n \n {ratingLabel}\n ·\n {opiniones}\n
\n
\n \n
\n
\n Ventas\n {ventas}\n
\n
\n Participantes\n {participantes}\n
\n
\n
\n
\n );\n }\n\n // ── \"dark-hero\" — plum profundo, stats XXL blancas con glow, formas de fondo ──\n if (layout === \"dark-hero\") {\n return (\n \n {/* formas: cuarto de círculo naranja + arcos + dots */}\n
\n
\n
\n
\n
\n
\n
\n \n {logo}\n
\n
\n

{nombre}

\n
\n {rating}\n \n {ratingLabel}\n ·\n {opiniones}\n
\n

{descripcion}

\n
\n
\n
\n Ventas\n {ventas}\n
\n \n
\n Participantes\n {participantes}\n
\n
\n
\n );\n }\n\n // ── \"photo\" — placeholder de imagen + gradiente de opacidad morado ──\n if (layout === \"photo\") {\n return (\n
\n {/* Placeholder de imagen (visible por la derecha) */}\n \n \n \n \n \n \n IMAGEN\n
\n {/* Gradiente de opacidad morado (marca) de izquierda a transparente */}\n \n
\n \n {logo}\n
\n
\n

{nombre}

\n
\n {rating}\n \n {ratingLabel}\n ·\n {opiniones}\n
\n
\n Ventas {ventas}\n Participantes {participantes}\n
\n
\n
\n
\n );\n }\n\n // ── \"panel\" — fondo morado del header DetailCard + StatPill reales ──\n if (layout === \"panel\") {\n return (\n \n {/* glass superior */}\n
\n {/* logo circular blanco */}\n \n {logo}\n
\n
\n

{nombre}

\n
\n {rating}\n \n {ratingLabel}\n ·\n {opiniones}\n
\n

{descripcion}

\n
\n {/* stats con los ESTILOS del StatPill (dark glass + borde VYStrokes) */}\n
\n \n \n
\n
\n );\n }\n\n if (layout === \"stats-bottom\") {\n return (\n
\n \n \n \n {/* Cabecera: logo + nombre + rating + reseña */}\n
\n \n
\n

{nombre}

\n
\n

{descripcion}

\n
\n
\n {/* Franja de stats inferior */}\n \n
\n Ventas\n {ventas}\n
\n
\n
\n Participantes\n {participantes}\n
\n
\n
\n );\n }\n\n // logo-left\n return (\n
\n \n
\n

{nombre}

\n
\n

{descripcion}

\n
\n
\n \n \n
\n
\n );\n}\n" }, { "path": "bloques/banners/desktop/PromoBanner.tsx", "type": "registry:file", "target": "bloques/banners/desktop/PromoBanner.tsx", "content": "/**\n * PromoBanner — 766×192 · variantes con los BACKGROUNDS de la referencia Subaspass\n * (carpeta /Banners). Paleta de marca promo:\n * · naranja #E8732A · naranja claro #F49B57 · naranja profundo #C85A1E\n * · morado #3D2299 · morado profundo #2A1670 · crema #FFE2C2\n * 4 direcciones de fondo A/B/C/D aplicadas a los banners de navegación.\n *\n * Jerarquía visual (de mayor a menor peso):\n * 1. número «Ofertas N» (crema, gigante) — el dato\n * 2. título con acento crema — el qué\n * 3. pill de contexto (EN VIVO / NEGOCIABLE) — la categoría\n * 4. chip glass de horario — el meta\n * Las SubasCoins son TEXTURA de fondo: contenidas dentro del banner, baja\n * opacidad, detrás del contenido — nunca compiten con el número ni se cortan.\n * Estático, sin efectos.\n */\n\nimport type { CSSProperties, JSX } from \"react\";\nimport TimerIcon from \"@/concorde/components/TimerIcon\";\nimport { HandshakeIcon, CarIcon, GemOutline } from \"./decor\";\nimport { BANNER_WIDTH, BANNER_HEIGHT } from \"./dimensions\";\n\nexport { BANNER_WIDTH, BANNER_HEIGHT } from \"./dimensions\";\n\n// ─── Paleta de marca (referencia Subaspass) ───────────────────────────────────\n\nconst C = {\n orange: \"#E8732A\",\n orangeLight: \"#F49B57\",\n orangeDeep: \"#C85A1E\",\n orangeDark: \"#B14E18\",\n plum: \"#3D2299\",\n plumDeep: \"#2A1670\",\n cream: \"#FFE2C2\",\n white: \"#FFFFFF\",\n};\n\n// ─── Backgrounds A/B/C/D ──────────────────────────────────────────────────────\n\nexport type PromoBg = \"orange\" | \"plum-orange\" | \"diagonal\" | \"orange-bold\";\n\nconst BACKGROUNDS: Record = {\n \"orange\": `linear-gradient(105deg, ${C.orange} 0%, ${C.orangeDeep} 100%)`,\n \"plum-orange\": `linear-gradient(100deg, ${C.plumDeep} 0%, ${C.plum} 32%, ${C.orangeDeep} 78%, ${C.orange} 100%)`,\n \"diagonal\": `linear-gradient(95deg, ${C.plumDeep} 0%, ${C.plum} 40%, ${C.orangeDeep} 100%)`,\n \"orange-bold\": `linear-gradient(120deg, ${C.orangeLight} 0%, ${C.orange} 45%, ${C.orangeDark} 100%)`,\n};\n\n// ─── Tonos para las variantes con tokens del DS ───────────────────────────────\n// live = vault gradiente del Button primary (#ed8936→#8460e5)\n// negotiable = el CELESTE del sistema negociable (#00D2D3/#00AEB1) → #8460e5\n\nexport type PromoTone = \"live\" | \"negotiable\";\n\ninterface ToneGrad {\n /** Gradiente token (fill del Button primary / negotiable) */\n token: string;\n /** Gradiente B invertido (color del tono a la izq → morado a la der) */\n flip: string;\n /** Fondo del layout centrado glass */\n centerBg: string;\n /** Fondo del layout con gema outline */\n gemBg: string;\n /** Fondo del layout \"mega\" (plum profundo → acento del tono) */\n megaBg: string;\n /** Tinte RGB del glass teñido (\"232,115,42\") */\n glassTint: string;\n /** Glow cálido/frío en RGB */\n glowWarm: string;\n glowCool: string;\n /** Stops del gradiente del tono para SVG (wave-split) */\n stopA: string;\n stopB: string;\n}\n\nconst TONE_GRADIENTS: Record = {\n live: {\n token: \"linear-gradient(135deg, #ED8936 0%, #ED8936 25%, #8460E5 100%)\",\n flip: `linear-gradient(100deg, ${C.orange} 0%, ${C.orangeDeep} 26%, ${C.plum} 72%, ${C.plumDeep} 100%)`,\n centerBg: `linear-gradient(100deg, ${C.plumDeep} 0%, ${C.plum} 32%, ${C.orangeDeep} 78%, ${C.orange} 100%)`,\n gemBg: `linear-gradient(105deg, ${C.orange} 0%, ${C.orangeDeep} 100%)`,\n megaBg: \"linear-gradient(115deg, #241262 0%, #3D2299 52%, #C85A1E 100%)\",\n glassTint: \"232,115,42\",\n glowWarm: \"255,226,194\",\n glowCool: \"174,142,255\",\n stopA: \"#ED8936\",\n stopB: \"#8460E5\",\n },\n negotiable: {\n token: \"linear-gradient(135deg, #00D2D3 0%, #00AEB1 25%, #8460E5 100%)\",\n flip: \"linear-gradient(100deg, #00D2D3 0%, #00AEB1 26%, #3D2299 72%, #2A1670 100%)\",\n centerBg: \"linear-gradient(100deg, #2A1670 0%, #3D2299 35%, #00838C 78%, #00D2D3 100%)\",\n gemBg: \"linear-gradient(105deg, #00D2D3 0%, #00939B 100%)\",\n megaBg: \"linear-gradient(115deg, #241262 0%, #3D2299 52%, #00939B 100%)\",\n glassTint: \"0,210,211\",\n glowWarm: \"178,246,246\",\n glowCool: \"174,142,255\",\n stopA: \"#00D2D3\",\n stopB: \"#8460E5\",\n },\n};\n\n// ─── Piezas ───────────────────────────────────────────────────────────────────\n\nconst shell = (bg: string): CSSProperties => ({\n position: \"relative\",\n width: BANNER_WIDTH,\n height: BANNER_HEIGHT,\n background: bg,\n overflow: \"hidden\",\n fontFamily: 'var(--vmc-font-display, \"Plus Jakarta Sans\", -apple-system, sans-serif)',\n color: C.white,\n flexShrink: 0,\n});\n\nfunction Pill({ text, icon, end }: { text: string; icon?: boolean; end?: boolean }): JSX.Element {\n return (\n \n {icon && }\n {text}\n \n );\n}\n\nfunction Headline({ pre, accent }: { pre: string; accent: string }): JSX.Element {\n return (\n

\n {pre} {accent}\n

\n );\n}\n\n/** Contador «Ofertas N» — el elemento de mayor jerarquía (número crema gigante) */\nfunction Counter({ count, size = 92 }: { count: number | string; size?: number }): JSX.Element {\n return (\n
\n Ofertas\n {count}\n
\n );\n}\n\nfunction PhotoSlot({ width, side, fadeColor }: { width: number; side: \"left\" | \"right\"; fadeColor: string }): JSX.Element {\n const clip = side === \"right\" ? \"polygon(18% 0, 100% 0, 100% 100%, 0 100%)\" : undefined;\n const fade =\n side === \"left\"\n ? `linear-gradient(90deg, transparent 48%, ${fadeColor} 100%)`\n : `linear-gradient(75deg, ${fadeColor} 0%, transparent 42%)`;\n return (\n
\n
\n \n \n \n \n \n FOTO\n
\n
\n
\n );\n}\n\nfunction DecoCircle(style: CSSProperties): JSX.Element {\n return
;\n}\n\nfunction Chip({ chip, end }: { chip: { label: string; icon?: \"car\" }; end?: boolean }): JSX.Element {\n return (\n \n {chip.label}\n {chip.icon === \"car\" && }\n \n );\n}\n\n/** Chip glass de horario — ancho de contenido (nunca se estira) */\nfunction TimerChip({ timer, end }: { timer: string; end?: boolean }): JSX.Element {\n return (\n \n \n {timer}\n \n );\n}\n\n/**\n * Coins de fondo — TEXTURA sutil, contenida en un área a la derecha detrás del\n * contador. Opacidad baja, blur ligero, recortada al banner (overflow hidden del\n * shell). Nunca compite con el número (va en z-index 0).\n */\n/**\n * Decor — decoración de fondo MODERNA (estática): luces radiales suaves,\n * círculos concéntricos y arcos finos. Va detrás del contenido (z-index 0);\n * el overflow:hidden del shell la recorta limpio en los bordes.\n * `warm`/`cool` = los dos colores del glow (crema cálida + un frío del tono).\n */\nfunction Decor({ warm = \"255,226,194\", cool = \"255,255,255\", cross = false, side = \"right\" }: { warm?: string; cool?: string; cross?: boolean; side?: \"left\" | \"right\" }): JSX.Element {\n // `side` = lado donde vive el contador (glow + rings van detrás de él)\n const main: CSSProperties = side === \"right\" ? { right: -40 } : { left: -40 };\n const ringA: CSSProperties = side === \"right\" ? { right: -70 } : { left: -70 };\n const ringB: CSSProperties = side === \"right\" ? { right: -30 } : { left: -30 };\n const dotA: CSSProperties = side === \"right\" ? { right: 200 } : { left: 200 };\n const dotB: CSSProperties = side === \"right\" ? { right: 260 } : { left: 260 };\n const arc: CSSProperties = side === \"right\" ? { left: -60 } : { right: -60 };\n return (\n
\n {/* Glow cálido grande detrás del número */}\n
\n {/* Glow frío superior */}\n
\n {/* Círculos concéntricos (ring) — mitad fuera */}\n
\n
\n {/* Dots */}\n
\n
\n {/* Arco extra opcional en la esquina opuesta */}\n {cross &&
}\n
\n );\n}\n\nfunction Sheen(): JSX.Element {\n return (\n \n );\n}\n\n// ─── Component ────────────────────────────────────────────────────────────────\n\nexport type PromoLayout =\n | \"photo-left\"\n | \"plum-counter\"\n | \"photo-right\"\n | \"bold-cta\"\n | \"flip\"\n | \"center-glass\"\n | \"token\"\n | \"gem-outline\"\n | \"mega\"\n | \"glass-tint\"\n | \"wave-split\"\n | \"orbit\"\n | \"echo\";\n\nexport interface PromoBannerProps {\n layout: PromoLayout;\n /** Tono para las variantes con tokens del DS (default \"live\") */\n tone?: PromoTone;\n pillText: string;\n pillIcon?: boolean;\n titlePre: string;\n titleAccent: string;\n count: number | string;\n timer?: string;\n chip?: { label: string; icon?: \"car\" };\n className?: string;\n}\n\nexport default function PromoBanner({\n layout,\n tone = \"live\",\n pillText,\n pillIcon = false,\n titlePre,\n titleAccent,\n count,\n timer,\n chip,\n className = \"\",\n}: PromoBannerProps): JSX.Element {\n const tg = TONE_GRADIENTS[tone];\n\n // ── FLIP · gradiente invertido (tono→morado) · contador IZQUIERDA ──\n if (layout === \"flip\") {\n return (\n
\n \n \n
\n \n
\n
\n \n \n {timer && }\n {chip && }\n
\n
\n );\n }\n\n // ── CENTER-GLASS · panel glass centrado con todo el contenido ──\n if (layout === \"center-glass\") {\n return (\n
\n \n \n \n
\n \n \n {timer && }\n {chip && }\n
\n \n \n
\n
\n );\n }\n\n // ── TOKEN · fondo = gradiente del Button primary / negotiable ──\n if (layout === \"token\") {\n return (\n
\n \n \n
\n \n \n {timer && }\n {chip && }\n
\n
\n \n
\n
\n );\n }\n\n // ── GEM-OUTLINE · gema SubasCoin en bordes blancos como forma de fondo ──\n if (layout === \"gem-outline\") {\n return (\n
\n {/* Gema outline gigante detrás del contador (se recorta elegante por la derecha) */}\n
\n \n \n
\n \n \n
\n \n \n {timer && }\n {chip && }\n
\n
\n \n
\n
\n );\n }\n\n // ── WAVE-SPLIT · divisor de onda curva: plum (contenido) | tono (número) ──\n if (layout === \"wave-split\") {\n const gid = `wave-${tone}`;\n return (\n
\n {/* mitad derecha con onda curva y gradiente del tono */}\n \n \n \n \n \n \n \n \n {/* filo claro de la onda */}\n \n \n {/* luces sutiles en el lado plum */}\n
\n
\n \n
\n \n \n {timer && }\n {chip && }\n
\n
\n Ofertas\n {count}\n
\n
\n );\n }\n\n // ── ORBIT · número al centro de anillos concéntricos (órbitas con dots) ──\n if (layout === \"orbit\") {\n return (\n
\n {/* órbitas centradas en el número (derecha) */}\n
\n {[124, 188, 252].map((d, i) => (\n
\n ))}\n {/* dots orbitando */}\n
\n
\n
\n {/* glow central */}\n
\n
\n \n
\n \n \n {timer && }\n {chip && }\n
\n
\n Ofertas\n {count}\n
\n
\n );\n }\n\n // ── ECHO · número con eco tipográfico (outline detrás + sólido delante) ──\n if (layout === \"echo\") {\n return (\n
\n {/* eco: número gigante en contorno detrás, desplazado */}\n \n {count}\n \n
\n
\n \n
\n \n \n {timer && }\n {chip && }\n
\n {/* número sólido delante, desplazado del eco */}\n
\n
\n Ofertas\n {count}\n
\n
\n
\n );\n }\n\n // ── MEGA · número BLANCO gigante + bandas diagonales glass ──\n if (layout === \"mega\") {\n return (\n
\n {/* bandas diagonales finas (textura glass) */}\n
\n {/* glow del tono detrás del número */}\n
\n \n
\n \n \n {timer && }\n {chip && }\n
\n
\n Ofertas\n {count}\n
\n
\n );\n }\n\n // ── GLASS-TINT · contador en tarjeta glass TEÑIDA del tono + formas ──\n if (layout === \"glass-tint\") {\n return (\n
\n {/* formas: cuarto de círculo del tono + arcos + dots + glow lila */}\n
\n
\n
\n
\n
\n
\n
\n
\n \n
\n \n \n {timer && }\n {chip && }\n
\n {/* tarjeta glass teñida del tono con el contador */}\n
\n \n Ofertas\n {count}\n
\n
\n
\n );\n }\n\n // ── A · foto izquierda · copy centro · contador derecha ──\n if (layout === \"photo-left\") {\n return (\n
\n \n \n \n
\n \n \n {timer && }\n {chip && }\n
\n
\n \n
\n
\n );\n }\n\n // ── B · morado→naranja · texto centro · contador derecha ──\n if (layout === \"plum-counter\") {\n return (\n
\n \n \n {DecoCircle({ left: 28, top: 22, width: 26, height: 26, border: \"2px solid rgba(255,255,255,0.16)\", zIndex: 1 })}\n {DecoCircle({ left: \"42%\", bottom: 18, width: 8, height: 8, background: \"rgba(255,255,255,0.16)\", zIndex: 1 })}\n
\n \n \n {timer && }\n {chip && }\n
\n
\n \n
\n
\n );\n }\n\n // ── C · copy + contador izquierda · foto derecha diagonal ──\n if (layout === \"photo-right\") {\n return (\n
\n \n \n
\n \n \n
\n \n {timer &&
}\n
\n {chip && }\n
\n \n
\n );\n }\n\n // ── D · naranja bold · copy izquierda · contador derecha (coins de fondo) ──\n return (\n
\n \n \n
\n \n \n {timer && }\n {chip && }\n
\n
\n \n
\n
\n );\n}\n" }, { "path": "components/StarIcon.tsx", "type": "registry:file", "target": "components/StarIcon.tsx", "content": "\"use client\";\n\n/**\n * StarIcon — Generado por Concorde\n * Fuente: Figma VOYAGER · \"StarIcons\" (nodo 2035:16598)\n *\n * Insignia de estrella 28×28: círculo con relleno gradiente naranja→lila\n * (#ED8936 → #8460E5), borde gradiente (white → #FBC47D → #AE8EFF → white) y\n * estrella blanca. Drop shadow naranja + inner shadow blanco (del filtro Figma).\n * Tamaño personalizable: \"sm\" (20px) · \"md\" (28px) · o número de px.\n */\n\nimport { useId } from \"react\";\nimport type { JSX } from \"react\";\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\nexport type StarIconSize = \"sm\" | \"md\";\n\nexport interface StarIconProps {\n /** Tamaño: \"sm\"=20px · \"md\"=28px · o número de px (default \"md\") */\n size?: StarIconSize | number;\n /** Texto accesible — si se omite, el ícono es decorativo (aria-hidden) */\n title?: string;\n className?: string;\n}\n\nconst SIZE_PX: Record = { sm: 20, md: 28 };\n\n// ─── Component ────────────────────────────────────────────────────────────────\n\nexport default function StarIcon({ size = \"md\", title, className = \"\" }: StarIconProps): JSX.Element {\n const uid = useId().replace(/:/g, \"-\");\n const px = typeof size === \"number\" ? size : SIZE_PX[size];\n\n return (\n \n {title ? {title} : null}\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n );\n}\n" }, { "path": "components/BusinessIcon.tsx", "type": "registry:file", "target": "components/BusinessIcon.tsx", "content": "\"use client\";\n\nimport { useId, useState } from \"react\";\nimport type { JSX } from \"react\";\n\nexport interface BusinessIconProps {\n size?: number;\n /** Controlled visual state — omit to let the icon track its own hover/active */\n state?: \"default\" | \"hover\" | \"active\";\n title?: string;\n className?: string;\n}\n\nexport default function BusinessIcon({ size = 24, state: controlled, title, className = \"\" }: BusinessIconProps): JSX.Element {\n const uid = useId().replace(/:/g, \"-\");\n const [hovered, setHovered] = useState(false);\n const [pressed, setPressed] = useState(false);\n\n const resolved = controlled ?? (pressed ? \"active\" : hovered ? \"hover\" : \"default\");\n const isActive = resolved === \"active\";\n const isHover = resolved === \"hover\";\n const paint = isActive ? \"white\" : `url(#${uid}g)`;\n\n const handlers = controlled ? {} : {\n onMouseEnter: () => setHovered(true),\n onMouseLeave: () => { setHovered(false); setPressed(false); },\n onMouseDown: () => setPressed(true),\n onMouseUp: () => setPressed(false),\n };\n\n return (\n \n {title && {title}}\n\n {!isActive && (\n \n {isHover ? (\n \n \n \n \n \n ) : (\n \n \n \n \n \n \n )}\n \n )}\n\n {/* Building — stroke-based icon */}\n \n \n );\n}\n" }, { "path": "components/TimerIcon.tsx", "type": "registry:file", "target": "components/TimerIcon.tsx", "content": "/**\n * TimerIcon — Generado por Concorde\n * Fuente: Figma VOYAGER · \"timericon\" (3201)\n *\n * Icono de temporizador/reloj (22×22), trazo redondeado 1.83. Copia exacta del\n * path del SVG. Tamaño y color personalizables (default currentColor).\n */\n\nimport type { JSX } from \"react\";\n\nexport interface TimerIconProps {\n /** Tamaño en px (default 22) */\n size?: number;\n /** Color del trazo (default \"currentColor\") */\n color?: string;\n /** Texto accesible. Si se omite, es decorativo (aria-hidden). */\n title?: string;\n className?: string;\n}\n\nexport default function TimerIcon({ size = 22, color = \"currentColor\", title, className = \"\" }: TimerIconProps): JSX.Element {\n return (\n \n {title ? {title} : null}\n \n \n );\n}\n" } ] }