feat(ui): drive mobile layouts from useIsMobile

This commit is contained in:
2026-04-21 22:46:07 -04:00
parent 16bbd9ab08
commit 7a917e5c22
16 changed files with 350 additions and 150 deletions

View File

@@ -159,10 +159,11 @@ describe("Event count badge positioning contract", () => {
});
describe("AI capture redesign", () => {
test("desktop composer treats prompt and attachments as peer panels", () => {
test("composer layout is driven by useIsMobile instead of Tailwind breakpoint classes", () => {
const source = readToolbarSource();
expect(source).toContain("lg:grid-cols-[minmax(0,1fr)_minmax(0,1fr)]");
expect(source).toContain("useIsMobile");
expect(source).not.toContain("lg:grid-cols-");
});
test("attachments panel is a first-class surfaced region, not an inline footer affordance", () => {
@@ -191,8 +192,8 @@ const ATTACH_BTN_CLASSES = "gap-1.5 text-xs";
/** Generate button: right side, primary variant, labeled */
const GENERATE_BTN_CLASSES = "gap-1.5 text-xs";
/** Info popover trigger: hidden on mobile, small on desktop so it stays secondary */
const INFO_TRIGGER_CLASSES = "hidden h-6 w-6 md:inline-flex";
/** Info popover trigger: compact affordance that only renders in desktop branches */
const INFO_TRIGGER_CLASSES = "h-8 w-8 text-muted-foreground/70 hover:text-foreground";
describe("Composer footer bar layout contract", () => {
test("footer row uses justify-between so Attach sits left and Generate sits right", () => {
@@ -223,16 +224,15 @@ describe("Composer footer bar layout contract", () => {
});
describe("Info popover trigger size contract", () => {
test("info trigger is hidden on mobile so keyboard-only guidance does not appear in touch layouts", () => {
const resolved = cn(INFO_TRIGGER_CLASSES);
expect(resolved).toContain("hidden");
expect(resolved).toContain("md:inline-flex");
test("info trigger is guarded by useIsMobile so keyboard-only guidance stays out of touch layouts", () => {
const source = readToolbarSource();
expect(source).toContain("!isMobile ? (");
});
test("info trigger is small (h-6 w-6) so it doesn't compete with Generate", () => {
test("info trigger stays visually secondary when rendered on desktop", () => {
const resolved = cn(INFO_TRIGGER_CLASSES);
expect(resolved).toContain("h-6");
expect(resolved).toContain("w-6");
expect(resolved).toContain("h-8");
expect(resolved).toContain("w-8");
});
});
@@ -328,7 +328,7 @@ describe("Multi-image strip layout contract", () => {
describe("AI textarea prompt input spacing contract", () => {
const TEXTAREA_BASE =
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm";
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50";
const AI_TEXTAREA_OVERRIDE =
"wrap-anywhere field-sizing-content resize-none w-full min-h-[2.5rem] max-h-64 overflow-y-auto bg-transparent border-0 shadow-none focus-visible:ring-0 focus-visible:ring-offset-0 px-3 py-1 text-sm placeholder:text-muted-foreground/60 placeholder:italic";