From b632eaea8d09a1c5ebf21fbd349a05d96feacd8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BA=95=E3=81=AA=E3=81=97=E6=B2=BC=E3=81=AE=E9=AD=94?= =?UTF-8?q?=E5=A5=B3?= Date: Tue, 9 Jun 2026 22:07:12 +0900 Subject: [PATCH 1/2] feat: implement Windows 11-like snap behavior for Window component - Add snap geometry module (src/hooks/snap.ts) with 8 zones - Extend useDraggable with snap lifecycle (preview, commit, cleanup) - Add snap props to Window (snapEnabled, snapThreshold) - Render blue semi-transparent preview overlay during drag - Implement z-index management for multi-window scenarios - Add snap release when dragging away from edge - Add pointercancel and unmount cleanup handlers - Fix 1px jitter on odd viewport sizes - Add comprehensive tests (262 passing) - Add Storybook snap scenarios Supported snap zones: left, right, top, bottom, top-left, top-right, bottom-left, bottom-right Closes window snap behavior feature --- .storybook/main.ts | 11 +- src/components/window/TitleBar.test.tsx | 5 + src/components/window/Window.module.css | 10 +- src/components/window/Window.stories.tsx | 134 +++++-- src/components/window/Window.test.tsx | 308 +++++++++++++++- src/components/window/Window.tsx | 71 +++- src/components/window/Window.wiring.test.tsx | 56 ++- src/hooks/snap.test.ts | 231 ++++++++++++ src/hooks/snap.ts | 77 ++++ src/hooks/useDraggable.test.ts | 355 +++++++++++++++++++ src/hooks/useDraggable.ts | 114 +++++- src/hooks/useResizable.ts | 3 +- 12 files changed, 1337 insertions(+), 38 deletions(-) create mode 100644 src/hooks/snap.test.ts create mode 100644 src/hooks/snap.ts diff --git a/.storybook/main.ts b/.storybook/main.ts index 78cf7b3..a3786d3 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -1,4 +1,5 @@ import type { StorybookConfig } from '@storybook/react-vite'; +import { mergeConfig } from 'vite'; const config: StorybookConfig = { "stories": [ @@ -12,6 +13,12 @@ const config: StorybookConfig = { "@storybook/addon-docs", "@storybook/addon-onboarding" ], - "framework": "@storybook/react-vite" + "framework": "@storybook/react-vite", + viteFinal: async (config) => + mergeConfig(config, { + build: { + cssMinify: false, + }, + }), }; -export default config; \ No newline at end of file +export default config; diff --git a/src/components/window/TitleBar.test.tsx b/src/components/window/TitleBar.test.tsx index 9d0e8a0..da9f88e 100644 --- a/src/components/window/TitleBar.test.tsx +++ b/src/components/window/TitleBar.test.tsx @@ -32,6 +32,11 @@ describe('TitleBar', () => { expect(screen.queryByRole('button', { name: 'Maximize' })).toBeNull(); }); + it('renders the Restore control as a button when maximized', () => { + render(); + expect(screen.getByRole('button', { name: 'Restore' }).tagName).toBe('BUTTON'); + }); + it('calls onMinimize when Minimize button is clicked', async () => { const onMinimize = vi.fn(); render(); diff --git a/src/components/window/Window.module.css b/src/components/window/Window.module.css index 15ebc5a..a610aad 100644 --- a/src/components/window/Window.module.css +++ b/src/components/window/Window.module.css @@ -41,10 +41,18 @@ transparent calc(100% - 12px), #ffffff calc(100% - 12px), #ffffff calc(100% - 11px), #808080 calc(100% - 11px), #808080 calc(100% - 10px), - transparent calc(100% - 10px) + transparent calc(100% - 10px) ); } +.snapPreview { + position: fixed; + background: rgba(0, 120, 212, 0.28); + border: 2px solid rgba(0, 120, 212, 0.65); + box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.35); + pointer-events: none; +} + /* Mobile/touch: increase hit area for resize handles */ @media (pointer: coarse) { .n, .s { height: 12px; } diff --git a/src/components/window/Window.stories.tsx b/src/components/window/Window.stories.tsx index ff42c25..27d7564 100644 --- a/src/components/window/Window.stories.tsx +++ b/src/components/window/Window.stories.tsx @@ -7,42 +7,79 @@ const meta: Meta = { component: Window, parameters: { layout: 'fullscreen', + docs: { + description: { + story: + 'Documented snap zones: left, right, top, bottom, top-left, top-right, bottom-left, and bottom-right. Snap Assist, Snap Groups, and keyboard shortcuts are not included.', + }, + }, + }, + argTypes: { + snapEnabled: { + control: 'boolean', + }, + snapThreshold: { + control: { type: 'number', min: 0, step: 1 }, + }, }, }; export default meta; type Story = StoryObj; +type WindowProps = React.ComponentProps; + +const windowArgs = { + onMinimize: () => {}, + onMaximize: () => {}, + onClose: () => {}, + snapEnabled: true, + snapThreshold: 20, +}; + +const renderWindow = (args: Story['args']) => { + const windowProps = args as WindowProps; + + return ( + +

This is the window body content.

+

Windows 98 style window!

+
+ ); +}; export const Default: Story = { args: { + ...windowArgs, title: 'My Documents', width: 480, height: 320, initialX: 40, initialY: 40, - onMinimize: () => {}, - onMaximize: () => {}, - onClose: () => {}, }, - render: (args) => ( - -

This is the window body content.

-

Windows 98 style window!

-
- ), + render: renderWindow, +}; + +export const SnapPlayground: Story = { + args: { + ...windowArgs, + title: 'Snap Playground', + width: 480, + height: 320, + initialX: 40, + initialY: 40, + }, + render: renderWindow, }; export const Inactive: Story = { args: { + ...windowArgs, title: 'Inactive Window', inactive: true, width: 400, height: 280, initialX: 80, initialY: 80, - onMinimize: () => {}, - onMaximize: () => {}, - onClose: () => {}, }, render: (args) => ( @@ -53,11 +90,10 @@ export const Inactive: Story = { export const Maximized: Story = { args: { + ...windowArgs, title: 'Maximized Window', maximized: true, - onMinimize: () => {}, onRestore: () => {}, - onClose: () => {}, }, render: (args) => ( @@ -68,15 +104,13 @@ export const Maximized: Story = { export const Minimized: Story = { args: { + ...windowArgs, title: 'Minimized Window', minimized: true, width: 400, height: 280, initialX: 80, initialY: 80, - onMinimize: () => {}, - onMaximize: () => {}, - onClose: () => {}, }, render: (args) => ( @@ -87,14 +121,12 @@ export const Minimized: Story = { export const WithStatusBar: Story = { args: { + ...windowArgs, title: 'My Computer', width: 560, height: 380, initialX: 60, initialY: 60, - onMinimize: () => {}, - onMaximize: () => {}, - onClose: () => {}, }, render: (args) => ( ), }; + +export const MultipleSnappingWindows: Story = { + args: { + ...windowArgs, + title: 'Primary Window', + width: 420, + height: 280, + initialX: 32, + initialY: 48, + }, + render: (args) => ( + <> + +

Drag me to any edge or corner to snap independently.

+
+ +

This second window snaps on its own too.

+
+ + ), +}; + +export const SnapDisabled: Story = { + args: { + ...windowArgs, + title: 'Snap Disabled', + snapEnabled: false, + width: 440, + height: 300, + initialX: 72, + initialY: 72, + }, + render: (args) => ( + +

Snapping is turned off for this window.

+
+ ), +}; + +export const CustomSnapThreshold: Story = { + args: { + ...windowArgs, + title: 'Custom Threshold', + snapThreshold: 40, + width: 440, + height: 300, + initialX: 72, + initialY: 72, + }, + render: (args) => ( + +

This window uses a wider snap threshold.

+
+ ), +}; diff --git a/src/components/window/Window.test.tsx b/src/components/window/Window.test.tsx index 26463d4..e1d05eb 100644 --- a/src/components/window/Window.test.tsx +++ b/src/components/window/Window.test.tsx @@ -1,6 +1,12 @@ -import { render, screen, waitFor } from '@testing-library/react'; +import { render, screen, waitFor, act } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { Window } from './Window'; +import styles from './Window.module.css'; + +function createPointerEvent(type: string, init: PointerEventInit) { + return new PointerEvent(type, init); +} describe('Window', () => { it('renders with title bar and window body', () => { @@ -20,6 +26,22 @@ describe('Window', () => { expect(windowEl.style.zIndex).toBe('99'); }); + it('raises the active window z-index when dragging starts', () => { + const { container } = render(); + const titleBar = screen.getByText('Test').closest('.title-bar') as HTMLElement; + titleBar.setPointerCapture = vi.fn(); + titleBar.releasePointerCapture = vi.fn(); + const windowEl = container.firstChild as HTMLElement; + + act(() => { + titleBar.dispatchEvent( + createPointerEvent('pointerdown', { clientX: 100, clientY: 100, pointerId: 1, bubbles: true }), + ); + }); + + expect(windowEl.style.zIndex).toBe('100'); + }); + it('renders status bar when provided', () => { render( Status content}> @@ -38,6 +60,201 @@ describe('Window', () => { expect(screen.queryByText('Status content')).toBeNull(); }); + describe('snap preview behavior', () => { + beforeEach(() => { + Object.defineProperty(window, 'innerWidth', { value: 1024, writable: true }); + Object.defineProperty(window, 'innerHeight', { value: 768, writable: true }); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('renders a preview while dragging into the right snap zone', async () => { + const { container } = render(); + const titleBar = screen.getByText('My Window').closest('.title-bar') as HTMLElement; + titleBar.setPointerCapture = vi.fn(); + titleBar.releasePointerCapture = vi.fn(); + + act(() => { + titleBar.dispatchEvent( + createPointerEvent('pointerdown', { clientX: 100, clientY: 100, pointerId: 1, bubbles: true }), + ); + window.dispatchEvent(createPointerEvent('pointermove', { clientX: 1014, clientY: 384, pointerId: 1 })); + }); + + await waitFor(() => { + expect(container.querySelector(`.${styles.snapPreview}`)).not.toBeNull(); + }); + + const preview = container.querySelector(`.${styles.snapPreview}`) as HTMLElement; + expect(preview).toHaveClass(styles.snapPreview); + expect(preview).toHaveAttribute('aria-hidden', 'true'); + expect(preview.getAttribute('role')).toBeNull(); + expect(preview.style.top).toBe('0px'); + expect(preview.style.left).toBe('512px'); + expect(preview.style.width).toBe('512px'); + expect(preview.style.height).toBe('768px'); + expect(preview.style.zIndex).toBe('101'); + expect(container.firstChild?.nextSibling).toBe(preview); + }); + + it('removes the preview after pointerup', async () => { + const { container } = render(); + const titleBar = screen.getByText('My Window').closest('.title-bar') as HTMLElement; + titleBar.setPointerCapture = vi.fn(); + titleBar.releasePointerCapture = vi.fn(); + + act(() => { + titleBar.dispatchEvent( + createPointerEvent('pointerdown', { clientX: 100, clientY: 100, pointerId: 1, bubbles: true }), + ); + window.dispatchEvent(createPointerEvent('pointermove', { clientX: 1014, clientY: 384, pointerId: 1 })); + window.dispatchEvent(createPointerEvent('pointerup', { clientX: 1014, clientY: 384, pointerId: 1 })); + }); + + await waitFor(() => { + expect(container.querySelector(`.${styles.snapPreview}`)).toBeNull(); + }); + }); + + it('removes the preview when leaving the snap zone before pointerup', async () => { + const { container } = render(); + const titleBar = screen.getByText('My Window').closest('.title-bar') as HTMLElement; + titleBar.setPointerCapture = vi.fn(); + titleBar.releasePointerCapture = vi.fn(); + + act(() => { + titleBar.dispatchEvent( + createPointerEvent('pointerdown', { clientX: 100, clientY: 100, pointerId: 1, bubbles: true }), + ); + window.dispatchEvent(createPointerEvent('pointermove', { clientX: 1014, clientY: 384, pointerId: 1 })); + window.dispatchEvent(createPointerEvent('pointermove', { clientX: 512, clientY: 384, pointerId: 1 })); + }); + + await waitFor(() => { + expect(container.querySelector(`.${styles.snapPreview}`)).toBeNull(); + }); + }); + + it('does not render a preview when snapEnabled is false', async () => { + const { container } = render(); + const titleBar = screen.getByText('My Window').closest('.title-bar') as HTMLElement; + titleBar.setPointerCapture = vi.fn(); + titleBar.releasePointerCapture = vi.fn(); + + act(() => { + titleBar.dispatchEvent( + createPointerEvent('pointerdown', { clientX: 100, clientY: 100, pointerId: 1, bubbles: true }), + ); + window.dispatchEvent(createPointerEvent('pointermove', { clientX: 1014, clientY: 384, pointerId: 1 })); + }); + + await waitFor(() => { + expect(container.querySelector(`.${styles.snapPreview}`)).toBeNull(); + }); + }); + + it('does not snap at 41px from the edge with snapThreshold=40', async () => { + const { container } = render( + , + ); + const titleBar = screen.getByText('My Window').closest('.title-bar') as HTMLElement; + titleBar.setPointerCapture = vi.fn(); + titleBar.releasePointerCapture = vi.fn(); + const windowEl = container.firstChild as HTMLElement; + + act(() => { + titleBar.dispatchEvent( + createPointerEvent('pointerdown', { clientX: 100, clientY: 100, pointerId: 1, bubbles: true }), + ); + window.dispatchEvent(createPointerEvent('pointermove', { clientX: 983, clientY: 384, pointerId: 1 })); + window.dispatchEvent(createPointerEvent('pointerup', { clientX: 983, clientY: 384, pointerId: 1 })); + }); + + await waitFor(() => { + expect(container.querySelector(`.${styles.snapPreview}`)).toBeNull(); + expect(windowEl.style.width).toBe('400px'); + expect(windowEl.style.height).toBe('300px'); + }); + }); + + it('snaps at 39px from the edge with snapThreshold=40', async () => { + const { container } = render( + , + ); + const titleBar = screen.getByText('My Window').closest('.title-bar') as HTMLElement; + titleBar.setPointerCapture = vi.fn(); + titleBar.releasePointerCapture = vi.fn(); + const windowEl = container.firstChild as HTMLElement; + + act(() => { + titleBar.dispatchEvent( + createPointerEvent('pointerdown', { clientX: 100, clientY: 100, pointerId: 1, bubbles: true }), + ); + window.dispatchEvent(createPointerEvent('pointermove', { clientX: 985, clientY: 384, pointerId: 1 })); + window.dispatchEvent(createPointerEvent('pointerup', { clientX: 985, clientY: 384, pointerId: 1 })); + }); + + await waitFor(() => { + expect(container.querySelector(`.${styles.snapPreview}`)).toBeNull(); + expect(windowEl.style.left).toBe('512px'); + expect(windowEl.style.top).toBe('0px'); + expect(windowEl.style.width).toBe('512px'); + expect(windowEl.style.height).toBe('768px'); + }); + }); + + it('does not show a snap preview while maximized and restores to the original size', async () => { + const onRestore = vi.fn(); + const { container } = render( + , + ); + const titleBar = screen.getByText('My Window').closest('.title-bar') as HTMLElement; + titleBar.setPointerCapture = vi.fn(); + titleBar.releasePointerCapture = vi.fn(); + const windowEl = container.firstChild as HTMLElement; + + act(() => { + titleBar.dispatchEvent( + createPointerEvent('pointerdown', { clientX: 100, clientY: 100, pointerId: 1, bubbles: true }), + ); + window.dispatchEvent(createPointerEvent('pointermove', { clientX: 1014, clientY: 384, pointerId: 1 })); + window.dispatchEvent(createPointerEvent('pointerup', { clientX: 1014, clientY: 384, pointerId: 1 })); + }); + + expect(container.querySelector(`.${styles.snapPreview}`)).toBeNull(); + + await userEvent.click(screen.getByRole('button', { name: 'Restore' })); + + await waitFor(() => { + expect(onRestore).toHaveBeenCalledOnce(); + expect(windowEl.style.width).toBe('400px'); + expect(windowEl.style.height).toBe('300px'); + }); + }); + + it('ignores pointerdown on title-bar buttons for snap dragging', async () => { + const { container } = render(); + const titleBar = screen.getByText('My Window').closest('.title-bar') as HTMLElement; + titleBar.setPointerCapture = vi.fn(); + titleBar.releasePointerCapture = vi.fn(); + const minimizeButton = screen.getByRole('button', { name: 'Minimize' }); + + act(() => { + minimizeButton.dispatchEvent( + createPointerEvent('pointerdown', { clientX: 10, clientY: 10, pointerId: 1, bubbles: true }), + ); + window.dispatchEvent(createPointerEvent('pointermove', { clientX: 1014, clientY: 384, pointerId: 1 })); + }); + + await waitFor(() => { + expect(titleBar.setPointerCapture).not.toHaveBeenCalled(); + expect(container.querySelector(`.${styles.snapPreview}`)).toBeNull(); + }); + }); + }); + it('calls onClose callback', async () => { const onClose = vi.fn(); render(); @@ -46,6 +263,95 @@ describe('Window', () => { expect(onClose).toHaveBeenCalledOnce(); }); + describe('snap commit behavior', () => { + beforeEach(() => { + Object.defineProperty(window, 'innerWidth', { value: 1024, writable: true }); + Object.defineProperty(window, 'innerHeight', { value: 768, writable: true }); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('snaps to the right half on commit', async () => { + const { container } = render(); + const titleBar = screen.getByText('My Window').closest('.title-bar') as HTMLElement; + titleBar.setPointerCapture = vi.fn(); + titleBar.releasePointerCapture = vi.fn(); + const windowEl = container.firstChild as HTMLElement; + + act(() => { + titleBar.dispatchEvent( + createPointerEvent('pointerdown', { clientX: 100, clientY: 100, pointerId: 1, bubbles: true }), + ); + window.dispatchEvent(createPointerEvent('pointermove', { clientX: 1014, clientY: 384, pointerId: 1 })); + window.dispatchEvent(createPointerEvent('pointerup', { clientX: 1014, clientY: 384, pointerId: 1 })); + }); + + await waitFor(() => { + expect(windowEl.style.left).toBe('512px'); + expect(windowEl.style.top).toBe('0px'); + expect(windowEl.style.width).toBe('512px'); + expect(windowEl.style.height).toBe('768px'); + }); + }); + + it('snaps to the top half on commit', async () => { + const { container } = render(); + const titleBar = screen.getByText('My Window').closest('.title-bar') as HTMLElement; + titleBar.setPointerCapture = vi.fn(); + titleBar.releasePointerCapture = vi.fn(); + const windowEl = container.firstChild as HTMLElement; + + act(() => { + titleBar.dispatchEvent( + createPointerEvent('pointerdown', { clientX: 100, clientY: 100, pointerId: 1, bubbles: true }), + ); + window.dispatchEvent(createPointerEvent('pointermove', { clientX: 512, clientY: 10, pointerId: 1 })); + window.dispatchEvent(createPointerEvent('pointerup', { clientX: 512, clientY: 10, pointerId: 1 })); + }); + + await waitFor(() => { + expect(windowEl.style.left).toBe('0px'); + expect(windowEl.style.top).toBe('0px'); + expect(windowEl.style.width).toBe('1024px'); + expect(windowEl.style.height).toBe('384px'); + }); + }); + + it('restores the floating size when dragging a snapped window away from the edge', async () => { + const { container } = render(); + const titleBar = screen.getByText('My Window').closest('.title-bar') as HTMLElement; + titleBar.setPointerCapture = vi.fn(); + titleBar.releasePointerCapture = vi.fn(); + const windowEl = container.firstChild as HTMLElement; + + act(() => { + titleBar.dispatchEvent( + createPointerEvent('pointerdown', { clientX: 100, clientY: 100, pointerId: 1, bubbles: true }), + ); + window.dispatchEvent(createPointerEvent('pointermove', { clientX: 1014, clientY: 384, pointerId: 1 })); + window.dispatchEvent(createPointerEvent('pointerup', { clientX: 1014, clientY: 384, pointerId: 1 })); + }); + + await waitFor(() => { + expect(windowEl.style.left).toBe('512px'); + expect(windowEl.style.top).toBe('0px'); + expect(windowEl.style.width).toBe('512px'); + expect(windowEl.style.height).toBe('768px'); + }); + + act(() => { + titleBar.dispatchEvent( + createPointerEvent('pointerdown', { clientX: 600, clientY: 100, pointerId: 2, bubbles: true }), + ); + }); + + expect(windowEl.style.width).toBe('400px'); + expect(windowEl.style.height).toBe('300px'); + }); + }); + describe('responsive sizing', () => { beforeEach(() => { Object.defineProperty(window, 'innerWidth', { value: 400, writable: true }); diff --git a/src/components/window/Window.tsx b/src/components/window/Window.tsx index 64f98ce..87ab5fc 100644 --- a/src/components/window/Window.tsx +++ b/src/components/window/Window.tsx @@ -24,6 +24,8 @@ export interface WindowProps { onMaximize?: () => void; onRestore?: () => void; onClose?: () => void; + snapEnabled?: boolean; + snapThreshold?: number; style?: CSSProperties; className?: string; zIndex?: number; @@ -71,6 +73,8 @@ export function Window({ onMaximize, onRestore, onClose, + snapEnabled = true, + snapThreshold = 20, style, className, zIndex, @@ -78,6 +82,10 @@ export function Window({ const titleBarRef = useRef(null); const [isMinimized, setIsMinimized] = useState(minimizedProp ?? false); const [isMaximized, setIsMaximized] = useState(maximizedProp ?? false); + const [activeZIndex, setActiveZIndex] = useState(zIndex ?? 1); + const preSnapPosition = useRef<{ x: number; y: number } | null>(null); + const preSnapSize = useRef<{ width: number; height: number } | null>(null); + const [isSnapped, setIsSnapped] = useState(false); // Calculate responsive initial size and position const responsiveInitial = useMemo( @@ -85,11 +93,13 @@ export function Window({ [width, height, initialX, initialY], ); + const snapActive = !isMaximized && snapEnabled; + // ドラッグ・リサイズで共有する単一ポジション state const [position, setPosition] = useState({ x: responsiveInitial.x, y: responsiveInitial.y }); // useResizable must come BEFORE useDraggable so we can use the live size for drag bounds - const { size, getResizeHandleProps } = useResizable({ + const { size, setSize, getResizeHandleProps } = useResizable({ initialWidth: responsiveInitial.width, initialHeight: responsiveInitial.height, initialX: responsiveInitial.x, @@ -103,14 +113,50 @@ export function Window({ }); // useDraggable uses the LIVE size from useResizable for bounds, not the initial size - const { dragHandleProps } = useDraggable({ + const { dragHandleProps, snapTarget } = useDraggable({ initialX: responsiveInitial.x, initialY: responsiveInitial.y, position, onPositionChange: setPosition, + snapEnabled: snapActive, + snapThreshold: snapActive ? snapThreshold : undefined, + minWidth: snapActive ? 200 : undefined, + minHeight: snapActive ? 100 : undefined, bounds: { width: size.width, height: size.height }, + onSnapCommit: snapActive + ? (target) => { + preSnapPosition.current = { x: position.x, y: position.y }; + preSnapSize.current = { width: size.width, height: size.height }; + setPosition({ x: target.x, y: target.y }); + setSize({ width: target.width, height: target.height }); + setIsSnapped(true); + } + : undefined, + onDragStart: () => { + setActiveZIndex((current) => current + 1); + + if (!isSnapped || !preSnapPosition.current || !preSnapSize.current) { + return; + } + + setPosition(preSnapPosition.current); + setSize(preSnapSize.current); + setIsSnapped(false); + }, }); + const snapPreviewStyle = + snapTarget && !isMinimized && !isMaximized + ? { + position: 'fixed' as const, + top: snapTarget.y, + left: snapTarget.x, + width: snapTarget.width, + height: snapTarget.height, + zIndex: activeZIndex + 1, + } + : null; + const handleMinimize = () => { setIsMinimized((prev) => !prev); onMinimize?.(); @@ -133,7 +179,7 @@ export function Window({ left: 4, width: 220, height: 'auto', - zIndex, + zIndex: activeZIndex, ...style, } : isMaximized @@ -143,7 +189,7 @@ export function Window({ left: 0, width: '100vw', height: '100vh', - zIndex, + zIndex: activeZIndex, ...style, } : { @@ -152,11 +198,11 @@ export function Window({ left: position.x, width: size.width, height: size.height, - zIndex, + zIndex: activeZIndex, ...style, }; - return ( + const windowRoot = (
); + + return ( + <> + {windowRoot} + {snapPreviewStyle && ( +