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 bidmessageUso
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
| Prop | Tipo | Default |
|---|---|---|
sideLado de la burbuja; define la esquina de la cola. | "sent" | "received" | "received" |
typeColor de la burbuja. | "live" | "vault" | "white" | sent→live · received→vault |
logoSlot opcional antes del texto. | ReactNode | — |
childrenContenido del mensaje. | ReactNode | "PROPUSO US$ 25,000" |
className | string | "" |
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>
</>
);
}