Fix mobile conference room composer

This commit is contained in:
Dotta committed 2026-06-16 10:02:04 +00:00
1 parent a5bae7012d
commit d0a3ca319a
3 files changed
+78 -41

No files matched your search

+42
View File
@@ -0,0 +1,42 @@
import { existsSync, readFileSync } from "node:fs";
import path from "node:path";
import { describe, expect, it } from "vitest";
/**
* PAP-54: the .typing-dots CSS block was silently dropped from index.css
* during a theme migration, leaving static markup with no animation. Guard
* the source so the block can't vanish again without failing a test. The
* browser-computed `animationName !== "none"` assertion lives in
* tests/e2e/conference-room-typing-intro.spec.ts.
*/
function readIndexCss(): string {
let dir = process.cwd();
for (let depth = 0; depth < 6; depth++) {
for (const candidate of [
path.join(dir, "src/index.css"),
path.join(dir, "ui/src/index.css"),
]) {
if (existsSync(candidate)) return readFileSync(candidate, "utf8");
}
dir = path.dirname(dir);
}
throw new Error("ui/src/index.css not found from " + process.cwd());
}
describe("typing-dots CSS animation guard (PAP-54 failure mode)", () => {
const css = readIndexCss();
it("keeps the bounce animation wired to .typing-dots span", () => {
const spanRules = [...css.matchAll(/\.typing-dots span\s*\{[^}]*\}/g)].map(
(match) => match[0],
);
expect(spanRules.length).toBeGreaterThan(0);
expect(
spanRules.some((rule) => /animation:\s*typing-bounce/.test(rule)),
).toBe(true);
});
it("keeps the typing-bounce keyframes", () => {
expect(css).toMatch(/@keyframes typing-bounce\s*\{/);
});
});
+26 -37
View File
@@ -1,7 +1,5 @@
// @vitest-environment jsdom
import { existsSync, readFileSync } from "node:fs";
import path from "node:path";
import { act, forwardRef, useImperativeHandle } from "react";
import type { ReactNode } from "react";
import { createRoot, type Root } from "react-dom/client";
@@ -267,43 +265,34 @@ describe("BoardChat staged typing intro", () => {
expect(hasWelcome(container)).toBe(true);
expect(hasTypingDots(container)).toBe(false);
});
});
describe("typing-dots CSS animation guard (PAP-54 failure mode)", () => {
// PAP-54: the .typing-dots CSS block was silently dropped from index.css
// during a theme migration, leaving static markup with no animation. Guard
// the source so the block can't vanish again without failing a test. The
// browser-computed `animationName !== "none"` assertion lives in
// tests/e2e/conference-room-typing-intro.spec.ts.
// Locate ui/src/index.css regardless of whether vitest runs from ui/ or
// the workspace root (import.meta.url is an http URL under jsdom, and the
// css pipeline swallows `?raw` imports — plain fs is the reliable path).
function readIndexCss(): string {
let dir = process.cwd();
for (let depth = 0; depth < 6; depth++) {
for (const candidate of [
path.join(dir, "src/index.css"),
path.join(dir, "ui/src/index.css"),
]) {
if (existsSync(candidate)) return readFileSync(candidate, "utf8");
}
dir = path.dirname(dir);
}
throw new Error("ui/src/index.css not found from " + process.cwd());
}
const css = readIndexCss();
it("reserves mobile viewport height and bottom-nav space for the composer", async () => {
await render();
it("keeps the bounce animation wired to .typing-dots span", () => {
const spanRules = [...css.matchAll(/\.typing-dots span\s*\{[^}]*\}/g)].map(
(m) => m[0],
const shell = container.querySelector(
'[data-testid="board-chat-shell"]',
) as HTMLDivElement | null;
expect(shell).not.toBeNull();
expect(shell?.className).toContain("h-[calc(100dvh_-_3rem_-_4rem");
expect(shell?.className).toContain("env(safe-area-inset-top)");
expect(shell?.className).toContain("env(safe-area-inset-bottom)");
expect(shell?.className).toContain("-m-4");
expect(shell?.className).toContain("md:h-[calc(100%_+_3rem)]");
const dock = container.querySelector(
'[data-testid="board-chat-composer-dock"]',
) as HTMLDivElement | null;
expect(dock).not.toBeNull();
expect(dock?.className).toContain("bottom-0");
expect(dock?.className).toContain("px-4");
expect(dock?.className).toContain("md:px-6");
const feedButton = container.querySelector(
'button[aria-label="Open agent feed"]',
) as HTMLButtonElement | null;
expect(feedButton).not.toBeNull();
expect(feedButton?.className).toContain(
"bottom-[calc(5rem_+_env(safe-area-inset-bottom))]",
);
expect(spanRules.length).toBeGreaterThan(0);
expect(
spanRules.some((rule) => /animation:\s*typing-bounce/.test(rule)),
).toBe(true);
});
it("keeps the typing-bounce keyframes", () => {
expect(css).toMatch(/@keyframes typing-bounce\s*\{/);
});
});
+10 -4
View File
@@ -668,7 +668,10 @@ export function BoardChat() {
}
return (
<div className="flex h-[calc(100%+3rem)] flex-col -m-6">
<div
data-testid="board-chat-shell"
className="flex h-[calc(100dvh_-_3rem_-_4rem_-_env(safe-area-inset-top)_-_env(safe-area-inset-bottom))] flex-col -m-4 md:h-[calc(100%_+_3rem)] md:-m-6"
>
<div
ref={splitContainerRef}
className="flex min-h-0 min-w-0 flex-1 flex-row"
@@ -734,7 +737,7 @@ export function BoardChat() {
>
{/* pb clears the floating glass dock (PAP-131) so the last bubble can
scroll fully above the composer. */}
<div className="flex flex-col gap-4 px-6 pt-3 pb-32">
<div className="flex flex-col gap-4 px-4 pt-3 pb-32 md:px-6">
{/* Typing bubble — shown unconditionally until the reveal
timer fires, so the animation is guaranteed to be
visible even while agent/goal data is still loading. */}
@@ -959,7 +962,10 @@ export function BoardChat() {
mask is gone — the dock carries the task-style soft top fade instead
(mirrors IssueChatThread's composer dock). pointer-events pass through
the fade so the scrollbar stays usable; the composer re-enables them. */}
<div className="pointer-events-none absolute inset-x-0 bottom-0 z-10 bg-gradient-to-t from-background via-background/95 to-background/0 px-6 pt-6 pb-5">
<div
data-testid="board-chat-composer-dock"
className="pointer-events-none absolute inset-x-0 bottom-0 z-10 bg-gradient-to-t from-background via-background/95 to-background/0 px-4 pt-6 pb-4 md:px-6 md:pb-5"
>
<ChatComposer
ref={composerRef}
value={input}
@@ -1004,7 +1010,7 @@ export function BoardChat() {
type="button"
size="icon"
variant="secondary"
className="fixed bottom-20 right-4 z-20 h-10 w-10 rounded-full shadow-lg"
className="fixed bottom-[calc(5rem_+_env(safe-area-inset-bottom))] right-4 z-20 h-10 w-10 rounded-full shadow-lg"
aria-label="Open agent feed"
>
<Activity className="h-4 w-4" />