mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-25 17:45:53 +01:00
Fix mobile conference room composer
This commit is contained in:
1 parent
a5bae7012d
commit
d0a3ca319a
3 files changed
+78
-41
No files matched your search
@@ -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*\{/);
|
||||
});
|
||||
});
|
||||
@@ -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*\{/);
|
||||
});
|
||||
});
|
||||
@@ -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" />
|
||||
|
||||
Reference in new issue
Block a user