From 43bdba29ec534e6fdb9beab483086d979b6f7b73 Mon Sep 17 00:00:00 2001 From: "paseo-bot[bot]" <266920839+paseo-bot[bot]@users.noreply.github.com> Date: Sun, 18 Oct 2026 17:06:02 +0200 Subject: [PATCH] fix(app): keep long sidebar actions inside narrow cards Wrapping the row still lets a single translated action exceed its available width. Bound each empty-state action to the row and allow its label to shrink and wrap. Co-authored-by: Vyacheslav Scherbinin --- .../sidebar/empty-states.browser.test.tsx | 56 +++++++++++++------ .../src/components/sidebar/empty-states.tsx | 32 ++++++++++- 2 files changed, 69 insertions(+), 19 deletions(-) diff --git a/packages/app/src/components/sidebar/empty-states.browser.test.tsx b/packages/app/src/components/sidebar/empty-states.browser.test.tsx index 6c9ecf2dff..85cd96c0e6 100644 --- a/packages/app/src/components/sidebar/empty-states.browser.test.tsx +++ b/packages/app/src/components/sidebar/empty-states.browser.test.tsx @@ -1,8 +1,9 @@ import React, { act } from "react"; import { createRoot, type Root } from "react-dom/client"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { getAllByRole } from "@testing-library/dom"; import { i18n as testI18n } from "@/i18n/i18next"; -import { DEFAULT_SIDEBAR_WIDTH } from "@/stores/panel-store"; +import { DEFAULT_SIDEBAR_WIDTH, MIN_SIDEBAR_WIDTH } from "@/stores/panel-store"; import { SidebarProjectEmptyState } from "./empty-states"; // App sources compile against the classic JSX runtime, which expects React on the global. @@ -28,6 +29,9 @@ async function mountEmptyState(language: string, width: number): Promise root.render()); @@ -40,22 +44,42 @@ async function mountEmptyState(language: string, width: number): Promise { - it.each([ - ["ru", DEFAULT_SIDEBAR_WIDTH], - ["en", DEFAULT_SIDEBAR_WIDTH], - ])("keeps every action inside the card (%s, %ipx sidebar)", async (language, width) => { - const card = await mountEmptyState(language, width); - const cardRect = card.getBoundingClientRect(); - const buttons = Array.from(card.querySelectorAll('[role="button"]')); +function expectActionsInsideCard(card: HTMLElement) { + const cardRect = card.getBoundingClientRect(); + const buttons = getAllByRole(card, "button"); - expect(buttons).toHaveLength(2); - for (const button of buttons) { - const rect = button.getBoundingClientRect(); - expect(rect.left).toBeGreaterThanOrEqual(cardRect.left); - expect(rect.right).toBeLessThanOrEqual(cardRect.right); + expect(buttons).toHaveLength(2); + for (const button of buttons) { + const rect = button.getBoundingClientRect(); + expect(rect.left).toBeGreaterThanOrEqual(cardRect.left); + expect(rect.right).toBeLessThanOrEqual(cardRect.right); + + // Containing the button must also keep the whole label readable, without clipping. + const label = document.createRange(); + label.selectNodeContents(button); + for (const fragment of Array.from(label.getClientRects())) { + expect(fragment.left).toBeGreaterThanOrEqual(rect.left); + expect(fragment.right).toBeLessThanOrEqual(rect.right); + expect(fragment.top).toBeGreaterThanOrEqual(rect.top); + expect(fragment.bottom).toBeLessThanOrEqual(rect.bottom); } + } +} + +describe("SidebarProjectEmptyState", () => { + const cases = ["en", "ru", "fr", "ja", "es", "pt-BR", "ar", "ko", "zh-CN"].flatMap((language) => + [MIN_SIDEBAR_WIDTH, DEFAULT_SIDEBAR_WIDTH].map((width) => ({ language, width })), + ); + it.each(cases)( + "keeps every action inside the card ($language, $width px sidebar)", + async ({ language, width }) => { + expectActionsInsideCard(await mountEmptyState(language, width)); + }, + ); + + it("keeps English actions on one row when they fit", async () => { + const card = await mountEmptyState("en", DEFAULT_SIDEBAR_WIDTH); + const [addProject, importSession] = getAllByRole(card, "button"); + expect(addProject.getBoundingClientRect().top).toBe(importSession.getBoundingClientRect().top); }); }); diff --git a/packages/app/src/components/sidebar/empty-states.tsx b/packages/app/src/components/sidebar/empty-states.tsx index 0e5daa7007..c9f6b7c568 100644 --- a/packages/app/src/components/sidebar/empty-states.tsx +++ b/packages/app/src/components/sidebar/empty-states.tsx @@ -54,7 +54,13 @@ export function SidebarFilterEmptyState() { title={t("sidebar.filterEmpty.title")} description={t("sidebar.filterEmpty.description")} > - @@ -76,10 +82,24 @@ export function SidebarProjectEmptyState({ title={t("sidebar.project.empty.title")} description={t("sidebar.project.empty.description")} > - - @@ -103,6 +123,12 @@ const styles = StyleSheet.create((theme) => ({ gap: theme.spacing[2], marginTop: theme.spacing[4], }, + action: { + maxWidth: "100%", + }, + actionText: { + flexShrink: 1, + }, title: { color: theme.colors.foreground, fontSize: theme.fontSize.sm,