From 67072a013d3abcdd9acc54ef4f15aa26da1e6732 Mon Sep 17 00:00:00 2001 From: Ilya Pashkov Date: Fri, 17 Jul 2026 19:32:31 +0900 Subject: [PATCH 1/5] fix(button): prevent label wrap and flex shrink across all button bases Buttons squeezed inside flex rows shrank below their intrinsic width and wrapped their labels. ButtonBase now carries whitespace-nowrap and shrink-0, covering Button, ToggleButton, TabsButton, and SegmentedControlButton. The fullWidth variant re-enables shrink (via tailwind-merge last-wins) so multiple fullWidth buttons can still split a row without overflowing. Co-Authored-By: Claude Fable 5 --- .../design-system/src/components/ButtonBase/classes.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/design-system/src/components/ButtonBase/classes.ts b/packages/design-system/src/components/ButtonBase/classes.ts index 3c6efd4a..9cd41173 100644 --- a/packages/design-system/src/components/ButtonBase/classes.ts +++ b/packages/design-system/src/components/ButtonBase/classes.ts @@ -3,8 +3,8 @@ import { cn } from '../../utils/cn'; export const buttonBaseVariants = cva( cn( - 'items-center justify-center min-w-0 rounded-8', - 'text-sm font-medium font-sans', + 'items-center justify-center min-w-0 shrink-0 rounded-8', + 'text-sm font-medium font-sans whitespace-nowrap', 'cursor-pointer overflow-hidden transition-[color,border,box-shadow,opacity]', 'focus-visible:outline-none focus-visible:ring-3', 'data-[focus=true]:outline-none data-[focus=true]:ring-3', @@ -31,7 +31,9 @@ export const buttonBaseVariants = cva( false: '', }, fullWidth: { - true: 'flex flex-1 basis-full w-full', + // shrink re-enables shrinking (overrides base shrink-0 via cn) so + // multiple fullWidth buttons can split a row without overflowing + true: 'flex flex-1 shrink basis-full w-full', false: 'inline-flex', }, }, From 80c3835fc19db6677098f2d18403e8e60358a04e Mon Sep 17 00:00:00 2001 From: Ilya Pashkov Date: Fri, 17 Jul 2026 19:32:41 +0900 Subject: [PATCH 2/5] fix(code): add primary-alt color variant Co-Authored-By: Claude Fable 5 --- packages/design-system/src/components/Code/Code.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/design-system/src/components/Code/Code.tsx b/packages/design-system/src/components/Code/Code.tsx index 1537ecf8..f2d73eb3 100644 --- a/packages/design-system/src/components/Code/Code.tsx +++ b/packages/design-system/src/components/Code/Code.tsx @@ -20,6 +20,7 @@ const codeVariants = cva('font-mono', { }, color: { primary: 'text-text-primary', + 'primary-alt': 'text-text-primary-alt', secondary: 'text-text-secondary', destructive: 'text-text-danger', }, From bacf20073463bd42f834da8e0a05da8d9c51e578 Mon Sep 17 00:00:00 2001 From: Ilya Pashkov Date: Fri, 17 Jul 2026 19:32:50 +0900 Subject: [PATCH 3/5] fix(tooltip): render tooltips above nested dialog/drawer overlays Tooltip content carried a static z-50, which zag's popper mirrors into the portal positioner - below any nested drawer or dialog positioner (50 + layer * 20), so a tooltip hovered inside a nested overlay rendered underneath it. Unlike Select/DropdownMenu/Popover (fixed in 0ad1c21f), tooltips never join zag's dismissable layer stack, so --layer-index is never set on the content and the layer-aware formula cannot apply. Tooltips instead get a dedicated top-tier --tooltip-z-index token (1000) that clears any realistic overlay stack. Adds a WithNestedDialog story plus an e2e stacking assertion via elementFromPoint (verified red -> green locally). Co-Authored-By: Claude Fable 5 --- .../src/components/Tooltip/Tooltip.e2e.ts | 29 +++++++++++ .../components/Tooltip/Tooltip.stories.tsx | 49 +++++++++++++++++++ .../src/components/Tooltip/TooltipContent.tsx | 9 +++- .../src/theme/components/tooltip.css | 8 +++ packages/design-system/src/theme/index.css | 1 + 5 files changed, 94 insertions(+), 2 deletions(-) create mode 100644 packages/design-system/src/theme/components/tooltip.css diff --git a/packages/design-system/src/components/Tooltip/Tooltip.e2e.ts b/packages/design-system/src/components/Tooltip/Tooltip.e2e.ts index b07054ff..c784ffdf 100644 --- a/packages/design-system/src/components/Tooltip/Tooltip.e2e.ts +++ b/packages/design-system/src/components/Tooltip/Tooltip.e2e.ts @@ -5,6 +5,7 @@ const tooltipStory = createStoryHelper('overlay-tooltip', [ 'Basic', 'With Description', 'With Kbd', + 'With Nested Dialog', ] as const); const getTooltipTrigger = (page: Page) => page.getByTestId('tooltip--trigger'); @@ -44,6 +45,34 @@ test.describe('Component: Tooltip', () => { await expect(getTooltipContent(page)).toHaveText('Right'); }); + test('Should stack the tooltip above a nested dialog when hovered inside it', async ({ + page, + }) => { + // Regression: the tooltip content's static z-50 mirrored into its + // positioner sat below the nested dialog positioner (50 + layer * 20), + // hiding the open tooltip. Tooltips never join zag's dismissable layer + // stack, so the content uses the dedicated top-tier --tooltip-z-index. + await tooltipStory.goto(page, 'With Nested Dialog'); + await page.getByRole('button', { name: 'Open dialog with nested tooltip' }).click(); + await page.getByRole('button', { name: 'Open nested dialog' }).click(); + await page.getByTestId('nested-tooltip--trigger').hover(); + + const content = page.getByTestId('nested-tooltip--content'); + await expect(content).toBeVisible(); + + // The topmost element at the tooltip's location must belong to the + // tooltip itself — not to the nested dialog covering it. + const isOnTop = await content.evaluate(el => { + const rect = el.getBoundingClientRect(); + const top = document.elementFromPoint( + rect.left + rect.width / 2, + rect.top + rect.height / 2, + ); + return !!top && el.contains(top); + }); + expect(isOnTop).toBe(true); + }); + test('Should hide tooltip when mouse leaves trigger', async ({ page }) => { await tooltipStory.goto(page, 'Basic'); await getTooltipTrigger(page).hover(); diff --git a/packages/design-system/src/components/Tooltip/Tooltip.stories.tsx b/packages/design-system/src/components/Tooltip/Tooltip.stories.tsx index e117b651..352ee4f0 100644 --- a/packages/design-system/src/components/Tooltip/Tooltip.stories.tsx +++ b/packages/design-system/src/components/Tooltip/Tooltip.stories.tsx @@ -1,6 +1,14 @@ import type { Meta, StoryFn } from 'storybook-react-rsbuild'; import { ChevronRight } from '../../icons'; import { Button } from '../Button'; +import { + Dialog, + DialogBody, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '../Dialog'; import { Kbd } from '../Kbd'; import { Text } from '../Text'; import { Tooltip } from './Tooltip'; @@ -45,6 +53,47 @@ export const WithDescription: StoryFn = () => ( ); +export const WithNestedDialog: StoryFn = () => ( + + + + + + + + [Level 1] Main Dialog + + + + + + + + + + + [Level 2] With Tooltip + + + + + + + + Tooltip above the dialog + + + + + + + +); + export const WithKbd: StoryFn = () => ( diff --git a/packages/design-system/src/components/Tooltip/TooltipContent.tsx b/packages/design-system/src/components/Tooltip/TooltipContent.tsx index 265b4310..70870ea5 100644 --- a/packages/design-system/src/components/Tooltip/TooltipContent.tsx +++ b/packages/design-system/src/components/Tooltip/TooltipContent.tsx @@ -10,8 +10,13 @@ const tooltipContentVariants = cva( [ // Base 'py-4 rounded-8 bg-component-tooltip-bg text-text-primary-alt text-xs font-medium', - // Behavior - 'z-50 overflow-hidden', + // Behavior: tooltips sit on a dedicated top tier above nested + // drawer/dialog positioners (50 + layer * 20). They can't reuse the + // layer-aware formula from DropdownMenu/Popover because zag never + // registers tooltips in its dismissable layer stack, so --layer-index + // is never set on this node. Zag's popper mirrors the computed z-index + // into the portal positioner's --z-index, so the value lives here. + 'z-(--tooltip-z-index) overflow-hidden', // Animation base 'animate-in fade-in-0 zoom-in-95 origin-[--transform-origin]', // Animation closed diff --git a/packages/design-system/src/theme/components/tooltip.css b/packages/design-system/src/theme/components/tooltip.css new file mode 100644 index 00000000..4f520cc7 --- /dev/null +++ b/packages/design-system/src/theme/components/tooltip.css @@ -0,0 +1,8 @@ +:root { + /* Tooltips never join zag's dismissable layer stack (unlike menus, + selects, popovers, and dialogs), so they cannot derive a layer-aware + z-index from --layer-index. Instead they live on a dedicated top tier + above any realistic overlay stack — drawer/dialog positioners compute + 50 + layer * 20, so 1000 clears dozens of nested levels. */ + --tooltip-z-index: 1000; +} diff --git a/packages/design-system/src/theme/index.css b/packages/design-system/src/theme/index.css index fce17941..62745353 100644 --- a/packages/design-system/src/theme/index.css +++ b/packages/design-system/src/theme/index.css @@ -19,6 +19,7 @@ @import './utilities/code-snippet-bg.css'; @import './components/drawer.css'; @import './components/dialog.css'; +@import './components/tooltip.css'; @import './components/tour.css'; @import './components/skeleton.css'; @import './components/accordion.css'; From 616624f6ea08762df856561de50efa057d8d3af1 Mon Sep 17 00:00:00 2001 From: Ilya Pashkov Date: Fri, 17 Jul 2026 19:42:26 +0900 Subject: [PATCH 4/5] fix(toast): render toasts above drawer/dialog stacks and their overlays The toaster region forced !z-[50], which sat at or below drawer/dialog positioners (50 + layer * 20) and nested overlays (40 + layer * 20), so toasts fired while any drawer or dialog was open rendered underneath it. The region now uses a dedicated top-tier --toast-z-index token (900) - above any realistic overlay stack, below --tooltip-z-index (1000). The important flag stays to beat zag's inline zIndex on the region; raising it is safe because zag sets pointer-events: none while the region holds no toasts. Adds a WithNestedOverlays story (drawer -> nested dialog, both firing toasts) plus an e2e stacking assertion via polled elementFromPoint (verified red -> green locally for both the nested and non-nested case). Co-Authored-By: Claude Fable 5 --- .../src/components/Toast/Toast.e2e.ts | 44 ++++++++++++ .../src/components/Toast/Toast.stories.tsx | 70 +++++++++++++++++++ .../src/components/Toast/Toaster.tsx | 7 +- .../src/theme/components/toast.css | 8 +++ packages/design-system/src/theme/index.css | 1 + 5 files changed, 129 insertions(+), 1 deletion(-) create mode 100644 packages/design-system/src/theme/components/toast.css diff --git a/packages/design-system/src/components/Toast/Toast.e2e.ts b/packages/design-system/src/components/Toast/Toast.e2e.ts index d6600f8f..843d9412 100644 --- a/packages/design-system/src/components/Toast/Toast.e2e.ts +++ b/packages/design-system/src/components/Toast/Toast.e2e.ts @@ -3,6 +3,7 @@ import { createStoryHelper } from '@wallarm-org/playwright-config/storybook'; const toastStory = createStoryHelper('messaging-toast', [ 'Basic', + 'With Nested Overlays', 'Update Loading To Success', 'Simple With Actions', 'Extended With Actions', @@ -200,4 +201,47 @@ test.describe('Toast Component', () => { await expect(page).toHaveScreenshot(); }); }); + + test.describe('Interactions', () => { + test('Should stack the toast above open drawer and nested dialog overlays', async ({ + page, + }) => { + // Regression: the toaster region's forced !z-[50] sat at or below the + // drawer/dialog positioners (50 + layer * 20) and nested overlays + // (40 + layer * 20), hiding toasts behind them. The region now sits on + // the dedicated top-tier --toast-z-index. + const expectFrontToastOnTop = async () => { + const toast = getToasts(page).first(); + await expect(toast).toBeVisible(); + + // The topmost element at the toast's location must belong to the + // toast itself — not to a drawer, dialog, or overlay covering it. + // Polled because the toast slides in: until the enter transition + // settles its center may still be outside the viewport. + await expect + .poll(() => + toast.evaluate(el => { + const rect = el.getBoundingClientRect(); + const top = document.elementFromPoint( + rect.left + rect.width / 2, + rect.top + rect.height / 2, + ); + return !!top && el.contains(top); + }), + ) + .toBe(true); + }; + + await toastStory.goto(page, 'With Nested Overlays'); + + await page.getByRole('button', { name: 'Open drawer' }).click(); + await page.getByRole('button', { name: 'Show toast from drawer' }).click(); + await expectFrontToastOnTop(); + + await page.getByRole('button', { name: 'Open nested dialog' }).click(); + await page.getByRole('button', { name: 'Show toast from nested dialog' }).click(); + await expect(getToasts(page)).toHaveCount(2); + await expectFrontToastOnTop(); + }); + }); }); diff --git a/packages/design-system/src/components/Toast/Toast.stories.tsx b/packages/design-system/src/components/Toast/Toast.stories.tsx index b9fcd889..9bf1d2b9 100644 --- a/packages/design-system/src/components/Toast/Toast.stories.tsx +++ b/packages/design-system/src/components/Toast/Toast.stories.tsx @@ -2,6 +2,22 @@ import { useRef } from 'react'; import type { Meta, StoryFn } from 'storybook-react-rsbuild'; import { Activity } from '../../icons'; import { Button } from '../Button'; +import { + Dialog, + DialogBody, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '../Dialog'; +import { + Drawer, + DrawerBody, + DrawerContent, + DrawerHeader, + DrawerTitle, + DrawerTrigger, +} from '../Drawer'; import { HStack, VStack } from '../Stack'; import { Text } from '../Text'; import { ToastActions, Toaster, useToast } from './index'; @@ -119,6 +135,60 @@ export const Basic: StoryFn = () => { return ; }; +export const WithNestedOverlays: StoryFn = () => { + const toast = useToast(); + + const fireToast = () => + toast.create({ + title: 'Saved', + description: 'This toast must render above every open overlay.', + type: 'success', + duration: STATIC_DURATION, + }); + + return ( + + + + + + + + [Level 1] Drawer + + + + + + + + + + + + + + [Level 2] Dialog + + + + + + + + + + + + ); +}; + export const UpdateLoadingToSuccess: StoryFn = () => { const toast = useToast(); const id = useRef(undefined); diff --git a/packages/design-system/src/components/Toast/Toaster.tsx b/packages/design-system/src/components/Toast/Toaster.tsx index 9f2666ce..54470f3d 100644 --- a/packages/design-system/src/components/Toast/Toaster.tsx +++ b/packages/design-system/src/components/Toast/Toaster.tsx @@ -104,7 +104,12 @@ export const Toaster: FC = () => { // center it so pausing tracks the toast column, not the full viewport width. // (These were previously on a `[data-part=group]` descendant selector that // never matched — the region element itself carries data-part="group".) - 'fixed bottom-0 right-0 !z-[50] flex flex-col gap-12 p-24 max-w-[560px] mx-auto', + // The `!` on z is required to beat zag's inline zIndex on the region; + // --toast-z-index keeps toasts above any drawer/dialog stack and its + // overlays (positioners compute 50 + layer * 20) while staying below + // tooltips. Safe to raise: zag sets pointer-events: none on the + // region while it holds no toasts. + 'fixed bottom-0 right-0 !z-(--toast-z-index) flex flex-col gap-12 p-24 max-w-[560px] mx-auto', // Root styles with CSS variables for animations '[&_[data-scope=toast][data-part=root]]:[translate:var(--x)_var(--y)] [&_[data-scope=toast][data-part=root]]:scale-[var(--scale)] [&_[data-scope=toast][data-part=root]]:z-[var(--z-index)] [&_[data-scope=toast][data-part=root]]:h-[var(--height)] [&_[data-scope=toast][data-part=root]]:opacity-[var(--opacity)] [&_[data-scope=toast][data-part=root]]:[will-change:translate,opacity,scale]', // Match Drawer/Dialog timings: 300ms open, 150ms close diff --git a/packages/design-system/src/theme/components/toast.css b/packages/design-system/src/theme/components/toast.css new file mode 100644 index 00000000..3acaf999 --- /dev/null +++ b/packages/design-system/src/theme/components/toast.css @@ -0,0 +1,8 @@ +:root { + /* Toasts are viewport-level notifications that must stay visible above + any open drawer/dialog stack and its overlays (positioners compute + 50 + layer * 20, overlays 40 + layer * 20), so they sit on a dedicated + top tier. Kept below --tooltip-z-index (1000) so tooltips triggered + from toast controls still render above the toast itself. */ + --toast-z-index: 900; +} diff --git a/packages/design-system/src/theme/index.css b/packages/design-system/src/theme/index.css index 62745353..64c2b333 100644 --- a/packages/design-system/src/theme/index.css +++ b/packages/design-system/src/theme/index.css @@ -20,6 +20,7 @@ @import './components/drawer.css'; @import './components/dialog.css'; @import './components/tooltip.css'; +@import './components/toast.css'; @import './components/tour.css'; @import './components/skeleton.css'; @import './components/accordion.css'; From 55a2c95a3a2454f476440c06b12a903b60e15ce7 Mon Sep 17 00:00:00 2001 From: Ilya Pashkov Date: Fri, 17 Jul 2026 19:53:14 +0900 Subject: [PATCH 5/5] fix(table): bump selection cell vertical padding to py-8 [update-screenshots] Body checkbox cells of the auto-injected selection column move from py-4 to py-8; the header checkbox cell keeps py-4. Co-Authored-By: Claude Fable 5 --- .../src/components/Table/lib/createSelectionColumn.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/design-system/src/components/Table/lib/createSelectionColumn.tsx b/packages/design-system/src/components/Table/lib/createSelectionColumn.tsx index a0184c88..04a33fa1 100644 --- a/packages/design-system/src/components/Table/lib/createSelectionColumn.tsx +++ b/packages/design-system/src/components/Table/lib/createSelectionColumn.tsx @@ -104,7 +104,7 @@ export const createSelectionColumn = (): ColumnDef => { enablePinning: false, meta: { headerClassName: 'px-8 py-4', - cellClassName: 'px-8 py-4', + cellClassName: 'px-8 py-8', }, header: ({ table }) => { const checked = table.getIsAllPageRowsSelected();