Concorde
BETA

BidMessage

Burbuja de mensaje de puja estilo chat, con lado y color por tipo.

PROPUSO US$ 25,000
PROPUSO US$ 25,000
<BidMessage side="sent" type="live">PROPUSO US$ 25,000</BidMessage>
<BidMessage side="received" type="vault">PROPUSO US$ 25,000</BidMessage>

En vivo (velocidad + distancia)

Los mensajes van llegando y entran animados. Ajusta la velocidad (duración del deslizamiento) y la distancia (cuánto recorre al aparecer).

Velocidad450 ms
Distancia48 px
"use client";

/**
 * Demo interactivo de BidMessage — simula el feed del chat: van llegando mensajes
 * y cada uno entra animado. Los sliders controlan la animación de entrada:
 *   · Velocidad → duración del deslizamiento (ms)
 *   · Distancia → cuánto recorre al aparecer (px)
 * Cada mensaje captura los valores al nacer, así mover los sliders solo afecta a
 * los siguientes (los ya visibles no re-animan).
 */

import { useEffect, useRef, useState } from "react";
import type { CSSProperties, JSX } from "react";
import BidMessage, { type BidMessageSide, type BidMessageType } from "@/src/components/BidMessage";

interface Msg {
  id: number;
  side: BidMessageSide;
  type: BidMessageType;
  text: string;
  dur: number;
  dist: number;
}

const USERS = ["KAHTH4", "ZAE389", "JA8NEE", "BEKVS1", "RDX12"];
const ARRIVAL_MS = 1400;
const MAX_VISIBLE = 6;

const KEYFRAMES = `
@keyframes bmsgdemo-in {
  from { opacity: 0; transform: translateX(var(--bmsg-d, 48px)); }
  to   { opacity: 1; transform: translateX(0); }
}
@media (prefers-reduced-motion: reduce) {
  .bmsgdemo-row { animation: none !important; }
}
`;

const sliderRow: CSSProperties = { display: "flex", alignItems: "center", gap: 12 };
const sliderLabel: CSSProperties = {
  fontSize: 11,
  fontWeight: 700,
  letterSpacing: "0.06em",
  textTransform: "uppercase",
  color: "rgba(255,255,255,0.5)",
  width: 78,
};
const sliderValue: CSSProperties = {
  fontSize: 12,
  fontWeight: 600,
  color: "rgba(255,255,255,0.8)",
  fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
  width: 54,
  textAlign: "right",
};

export default function BidMessageDemo(): JSX.Element {
  const [speed, setSpeed] = useState(450); // ms de duración del slide-in
  const [distance, setDistance] = useState(48); // px de recorrido
  const [msgs, setMsgs] = useState<Msg[]>([]);

  const idRef = useRef(0);
  const speedRef = useRef(speed);
  const distRef = useRef(distance);

  // Mantiene los refs sincronizados sin reiniciar el stream.
  useEffect(function sync() {
    speedRef.current = speed;
    distRef.current = distance;
  }, [speed, distance]);

  useEffect(function stream() {
    const t = setInterval(function arrive() {
      setMsgs(function next(prev) {
        const i = idRef.current;
        idRef.current = i + 1;
        const sent = i % 2 === 0;
        const side: BidMessageSide = sent ? "sent" : "received";
        const type: BidMessageType = sent ? "live" : i % 4 === 1 ? "white" : "vault";
        const user = USERS[i % USERS.length];
        const amount = (25 + i) * 1000;
        const m: Msg = {
          id: i,
          side,
          type,
          text: `${user} · PROPUSO US$ ${amount.toLocaleString("en-US")}`,
          dur: speedRef.current,
          dist: distRef.current,
        };
        return [...prev, m].slice(-MAX_VISIBLE);
      });
    }, ARRIVAL_MS);
    return function cleanup() { clearInterval(t); };
  }, []);

  return (
    <div style={{ display: "flex", flexDirection: "column", gap: 18, width: "100%", maxWidth: 420 }}>
      <style dangerouslySetInnerHTML={{ __html: KEYFRAMES }} />

      {/* Feed */}
      <div
        style={{
          height: 250,
          display: "flex",
          flexDirection: "column",
          justifyContent: "flex-end",
          gap: 8,
          overflow: "hidden",
          padding: "8px 4px",
        }}
      >
        {msgs.map(function renderMsg(m) {
          return (
            <div
              key={m.id}
              className="bmsgdemo-row"
              style={{
                display: "flex",
                justifyContent: m.side === "sent" ? "flex-end" : "flex-start",
                // sent entra desde la derecha (+), received desde la izquierda (−)
                ["--bmsg-d" as string]: `${m.side === "sent" ? m.dist : -m.dist}px`,
                animation: `bmsgdemo-in ${m.dur}ms cubic-bezier(0.22,1,0.36,1) both`,
              }}
            >
              <BidMessage side={m.side} type={m.type}>{m.text}</BidMessage>
            </div>
          );
        })}
      </div>

      {/* Controles */}
      <div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
        <div style={sliderRow}>
          <span style={sliderLabel}>Velocidad</span>
          <input
            type="range"
            min={120}
            max={1200}
            step={30}
            value={speed}
            onChange={function onSpeed(e) { setSpeed(Number(e.target.value)); }}
            style={{ flex: 1, accentColor: "#8460E5" }}
          />
          <span style={sliderValue}>{speed} ms</span>
        </div>
        <div style={sliderRow}>
          <span style={sliderLabel}>Distancia</span>
          <input
            type="range"
            min={0}
            max={140}
            step={4}
            value={distance}
            onChange={function onDist(e) { setDistance(Number(e.target.value)); }}
            style={{ flex: 1, accentColor: "#8460E5" }}
          />
          <span style={sliderValue}>{distance} px</span>
        </div>
      </div>
    </div>
  );
}

Instalación

$ npx github:AaronCoorahua/ConcordeV2#cli add bidmessage

Uso

import BidMessage from "@/src/components/BidMessage";

<BidMessage side="sent" type="live">PROPUSO US$ 25,000</BidMessage>

Ejemplos

Sent · live

Lado derecho, relleno naranja.

PROPUSO US$ 25,000
<BidMessage side="sent" type="live">PROPUSO US$ 25,000</BidMessage>

Received · vault

Lado izquierdo, relleno morado.

PROPUSO US$ 25,000
<BidMessage side="received" type="vault">PROPUSO US$ 25,000</BidMessage>

Received · white

Burbuja blanca con texto morado.

PROPUSO US$ 25,000
<BidMessage side="received" type="white">PROPUSO US$ 25,000</BidMessage>

API

PropTipoDefault
side
Lado de la burbuja; define la esquina de la cola.
"sent" | "received""received"
type
Color de la burbuja.
"live" | "vault" | "white"sent→live · received→vault
logo
Slot opcional antes del texto.
ReactNode
children
Contenido del mensaje.
ReactNode"PROPUSO US$ 25,000"
classNamestring""

Código del componente

Ver BidMessage.tsx completo · self-contained · zero deps
BidMessage.tsx
"use client";

/**
 * BidMessage — Generado por Concorde
 * Fuente: Figma VOYAGER · "BidMessage" (3162:12972 / 12990 / 13002 / 12984)
 *
 * Burbuja de mensaje de puja, estilo chat. Dos lados (cola en la esquina
 * inferior del lado):
 *   · side="sent"      → derecha (cola inferior-derecha)
 *   · side="received"  → izquierda (cola inferior-izquierda)
 * Color por `type`:
 *   · "live"   → relleno naranja (#FF9639→#BE3D00) + borde gradiente, texto blanco
 *   · "vault"  → relleno morado (#19004A→#3B1782→#2E0F70), texto blanco
 *   · "white"  → relleno blanco + borde lila, texto morado #3B1782
 * `logo` (slot) para la variante con logo. `children` = el mensaje.
 * Por convención: sent → live; received → vault/white. (type por defecto según side.)
 */

import type { JSX, ReactNode } from "react";

export type BidMessageSide = "sent" | "received";
export type BidMessageType = "live" | "vault" | "white";

export interface BidMessageProps {
  /** Lado de la burbuja (default "received") */
  side?: BidMessageSide;
  /** Color (default: sent→"live", received→"vault") */
  type?: BidMessageType;
  /** Logo opcional (slot) antes del texto */
  logo?: ReactNode;
  /** Contenido del mensaje */
  children?: ReactNode;
  className?: string;
}

const STYLE_ID = "concorde-bidmessage-styles";

const BIDMESSAGE_STYLES = `
.pbidmsg {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  min-height: 40px;
  max-width: 100%;
  box-sizing: border-box;
  padding: 8px 18px;
  font-family: var(--vmc-font-display, "Plus Jakarta Sans", -apple-system, sans-serif);
  font-size: 14px;
  font-weight: 500;
  line-height: 1.3;
}
.pbidmsg__logo { display: inline-flex; align-items: center; flex-shrink: 0; }
.pbidmsg__text { white-space: nowrap; }

/* Cola según el lado */
.pbidmsg--received { border-radius: 20px 20px 20px 4px; }
.pbidmsg--sent { border-radius: 20px 20px 4px 20px; }

/* live (naranja) — sent */
.pbidmsg--live {
  border: 1.5px solid transparent;
  background-image:
    linear-gradient(180deg, #FF9639 0%, #EF852E 40%, #BE3D00 100%),
    linear-gradient(120deg, #ffffff 0%, #F4AC59 22%, #8460E5 74.5%, #ffffff 100%);
  background-origin: border-box;
  background-clip: padding-box, border-box;
  color: #ffffff;
  box-shadow: rgba(225,108,16,0.3) 0px 2px 12px;
}

/* vault (morado) — received */
.pbidmsg--vault {
  border: none;
  background: linear-gradient(90deg, #19004A 0%, #3B1782 50%, #2E0F70 100%);
  color: #ffffff;
  box-shadow: rgba(46,15,112,0.35) 0px 4px 16px;
}

/* white (blanco, texto morado) — received */
.pbidmsg--white {
  border: 1.5px solid transparent;
  background-image:
    linear-gradient(#ffffff, #ffffff),
    linear-gradient(135deg, #CFBAFF 0%, #ffffff 35%, #AE8EFF 65%, #CFBAFF 100%);
  background-origin: border-box;
  background-clip: padding-box, border-box;
  color: #3B1782;
  box-shadow: rgba(90,53,194,0.5) 0px 2px 10px;
}
`;

let _stylesInjected = false;

export default function BidMessage({
  side = "received",
  type,
  logo,
  children = "PROPUSO US$ 25,000",
  className = "",
}: BidMessageProps): JSX.Element {
  const resolvedType: BidMessageType = type ?? (side === "sent" ? "live" : "vault");

  if (typeof document !== "undefined" && !_stylesInjected) {
    if (!document.getElementById(STYLE_ID)) {
      const el = document.createElement("style");
      el.id = STYLE_ID;
      el.textContent = BIDMESSAGE_STYLES;
      document.head.appendChild(el);
    }
    _stylesInjected = true;
  }

  const cls = ["pbidmsg", `pbidmsg--${side}`, `pbidmsg--${resolvedType}`, className].filter(Boolean).join(" ");

  return (
    <>
      <style id={`${STYLE_ID}-ssr`} suppressHydrationWarning dangerouslySetInnerHTML={{ __html: BIDMESSAGE_STYLES }} />
      <div className={cls}>
        {logo ? <span className="pbidmsg__logo">{logo}</span> : null}
        <span className="pbidmsg__text">{children}</span>
      </div>
    </>
  );
}