Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions server/src/agent/loop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,12 @@ créalo con bash: es tu trabajo, no preguntes por dónde empezar.
- Deja el dev server en el script "dev" del package.json, escuchando en el puerto de la
variable PORT y con el host abierto. Multi lo levanta y lo muestra a toda la sala;
sin eso nadie ve nada.
- Lo que construyas tiene que verse bien también en un teléfono, no solo en pantalla
ancha: nada de anchos fijos en el layout, y que el texto se lea y los botones se
puedan tocar con el dedo.
Por qué: la sala mira el preview desde donde sea, y lo que se publica acaba en un link
que la gente abre en el celular. Una app que solo se ve bien en la computadora está a
medias.
- El proyecto vive en un volumen montado, donde los eventos de archivo del sistema no
cruzan. Configura el watcher de tu stack por SONDEO (polling) o los cambios no se
verán en vivo y la sala se quedará mirando una pantalla congelada.
Expand Down
96 changes: 90 additions & 6 deletions web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,25 @@ import {
/** Cuántas imágenes caben en un mensaje. El server aplica el mismo tope. */
const MAX_ADJUNTOS = 4;

/**
* A qué ancho se mira el preview.
*
* Un solo botón las cicla en este orden, en vez de tres botones en la barra:
* ya hay siete controles ahí arriba y este se toca poco.
*
* Los anchos son los de siempre para cada clase de pantalla; lo que importa no
* es el número exacto sino cruzar los puntos donde un layout se rompe.
*/
const VISTAS = ["escritorio", "tablet", "movil"] as const;
type Vista = (typeof VISTAS)[number];

const ANCHO_DE_VISTA: Record<Vista, string | undefined> = {
// Sin ancho: el preview ocupa lo que haya, como siempre.
escritorio: undefined,
tablet: "768px",
movil: "390px",
};

// El roomId vive en el hash de la URL: #/sala/taco-fiesta-42
function readRoomFromHash(): string | null {
const m = window.location.hash.match(/#\/sala\/([\w-]+)/);
Expand Down Expand Up @@ -227,6 +246,13 @@ function Sala({ roomId, name }: { roomId: string | null; name: string }) {
* no cabe al lado del preview y pasa a ser una vista más.
*/
const [tab, setTab] = useState<"chat" | "app" | "back">("app");
/**
* A qué ancho se está viendo el preview.
*
* Es solo del que mira: cambiarlo no le mueve nada a los demás de la sala, ni
* toca la app. Se angosta el marco, no el proyecto.
*/
const [vista, setVista] = useState<Vista>("escritorio");
/**
* El chat colapsado deja el preview a pantalla completa.
*
Expand Down Expand Up @@ -990,6 +1016,20 @@ function Sala({ roomId, name }: { roomId: string | null; name: string }) {
<button className={`tab ${tab === "back" ? "activa" : ""}`} onClick={() => setTab("back")}>
{t.elBack}
</button>

{/* A qué ancho se ve el preview. Un solo botón que cicla, y no tres,
porque se toca poco y la barra de arriba ya va llena. Solo aparece
con la app a la vista: en el back no hay nada que redimensionar. */}
{tab === "app" && (
<button
className="vista-btn"
onClick={() => setVista(VISTAS[(VISTAS.indexOf(vista) + 1) % VISTAS.length])}
title={t.verEn[vista]}
aria-label={t.verEn[vista]}
>
<IconoDeVista vista={vista} />
</button>
)}
</div>

{tab === "back" && roomId && (
Expand All @@ -1001,12 +1041,17 @@ function Sala({ roomId, name }: { roomId: string | null; name: string }) {
<div className="lienzo" style={tab === "back" ? { display: "none" } : undefined}>
{previewReady ? (
<>
<iframe
ref={iframeRef}
className="preview-frame"
src={previewSrc}
title={t.tituloPreview}
/>
{/* El ancho va en el marco, no en el iframe, y el iframe NUNCA se
desmonta: recargarlo perdería el estado de la app (formularios
a medias, en qué pantalla ibas) y costaría segundos. */}
<div className="preview-marco" style={{ maxWidth: ANCHO_DE_VISTA[vista] }}>
<iframe
ref={iframeRef}
className="preview-frame"
src={previewSrc}
title={t.tituloPreview}
/>
</div>
{/* Selecciones de OTROS (con su color/nombre). Nota: se dibujan
como badges de aviso; el outline exacto vive dentro del iframe. */}
<div className="others-selections">
Expand Down Expand Up @@ -1081,6 +1126,45 @@ function Sala({ roomId, name }: { roomId: string | null; name: string }) {
* dos es del sistema (los avisos siempre se ven aparte). Los anclados tampoco
* se agrupan: llevan su propia nota de contexto.
*/
/**
* El icono del botón: dice a qué ancho se está viendo ahora.
*
* Van dibujados y no como emoji para que se vean igual en todos lados, y para
* que hereden el color del botón como el resto de la interfaz.
*/
function IconoDeVista({ vista }: { vista: Vista }) {
// Un rectángulo por cada clase de pantalla, con sus proporciones: el monitor
// ancho, la tablet casi cuadrada, el teléfono alto y angosto.
const caja =
vista === "escritorio"
? { x: 2, y: 4, w: 20, h: 14, r: 2 }
: vista === "tablet"
? { x: 5, y: 3, w: 14, h: 18, r: 2 }
: { x: 7, y: 2, w: 10, h: 20, r: 2 };

return (
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<rect
x={caja.x}
y={caja.y}
width={caja.w}
height={caja.h}
rx={caja.r}
stroke="currentColor"
strokeWidth="1.8"
/>
{/* El pie del monitor, que es lo que lo distingue de una tablet apaisada. */}
{vista === "escritorio" && (
<path d="M9 21h6M12 18v3" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" />
)}
{/* El botón del teléfono, por lo mismo. */}
{vista === "movil" && (
<path d="M11 19h2" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" />
)}
</svg>
);
}

function esSeguido(msgs: ChatMessage[], i: number): boolean {
if (i === 0) return false;
const prev = msgs[i - 1];
Expand Down
11 changes: 11 additions & 0 deletions web/src/i18n.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,12 @@ const TEXTOS = {
/** Pestaña del chat: solo se ve en pantallas chicas. */
elChat: "El chat",
laApp: "La app",
/** Qué se ve ahora, en el botón que cicla los anchos del preview. */
verEn: {
escritorio: "Viendo en computadora — clic para tablet",
tablet: "Viendo en tablet — clic para celular",
movil: "Viendo en celular — clic para computadora",
} as Record<string, string>,
elBack: "El back",

// Preview
Expand Down Expand Up @@ -284,6 +290,11 @@ const TEXTOS = {
// Pestañas
elChat: "Chat",
laApp: "The app",
verEn: {
escritorio: "Desktop view — click for tablet",
tablet: "Tablet view — click for mobile",
movil: "Mobile view — click for desktop",
} as Record<string, string>,
elBack: "The back",

// Preview
Expand Down
46 changes: 46 additions & 0 deletions web/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -2123,3 +2123,49 @@ body::after {
max-width: 100%;
}
}

/* ── Ver la app a otro ancho ──────────────────────────────────────────────
El preview se angosta para ver cómo queda la app en una tablet o un teléfono.
Es solo del que mira: no toca el proyecto ni le cambia nada a la sala. */
.preview-marco {
/* Sin límite es el ancho de siempre; con él, se centra y deja ver los bordes
para que se note que está angostado a propósito. */
width: 100%;
height: 100%;
margin: 0 auto;
/* La transición hace evidente que cambió de tamaño, en vez de un salto que
parece que se recargó. */
transition: max-width 0.2s ease;
}

/* El botón vive entre las pestañas, así que se separa de ellas: no es una
vista más, es cómo se mira la que ya está abierta. */
.vista-btn {
margin-left: auto;
align-self: center;
background: transparent;
border: 1px solid var(--linea);
border-radius: 4px;
color: var(--dim);
cursor: pointer;
padding: 6px 8px;
display: inline-flex;
align-items: center;
justify-content: center;
transition: color 0.15s, border-color 0.15s;
}
.vista-btn:hover {
color: var(--texto);
border-color: var(--dim);
}

@media (max-width: 820px) {
/* En un teléfono ya estás viendo el ancho de un teléfono: angostarlo más no
dice nada, y el botón solo quitaría espacio a las pestañas. */
.vista-btn {
display: none;
}
.preview-marco {
max-width: 100% !important;
}
}
Loading