From f971df37db8aceb062d9871cad7dc7c9e11975de Mon Sep 17 00:00:00 2001 From: Marian Rudzynski Date: Tue, 29 Jul 2025 02:30:00 +0000 Subject: [PATCH 01/11] feat: implement drawer component to replace FullscreenModal in EntityBrowser --- src/components/EntityBrowser.tsx | 31 ++- .../__tests__/EntityBrowser.test.tsx | 13 +- src/components/ui/Drawer.tsx | 136 ++++++++++++ src/components/ui/drawer.css | 196 ++++++++++++++++++ src/components/ui/index.ts | 1 + 5 files changed, 349 insertions(+), 28 deletions(-) create mode 100644 src/components/ui/Drawer.tsx create mode 100644 src/components/ui/drawer.css diff --git a/src/components/EntityBrowser.tsx b/src/components/EntityBrowser.tsx index 76e05c6..8bc88a6 100644 --- a/src/components/EntityBrowser.tsx +++ b/src/components/EntityBrowser.tsx @@ -1,7 +1,6 @@ import { useCallback } from 'react' -import { Tabs, Box, Flex, Button, Card, Text } from '@radix-ui/themes' -import { Cross2Icon } from '@radix-ui/react-icons' -import { FullscreenModal } from './ui' +import { Tabs, Box, Flex, Card, Text } from '@radix-ui/themes' +import { Drawer } from './ui' import { EntitiesBrowserTab } from './EntitiesBrowserTab' import { CardsBrowserTab } from './CardsBrowserTab' @@ -17,18 +16,23 @@ export function EntityBrowser({ open, onOpenChange, screenId }: EntityBrowserPro }, [onOpenChange]) return ( - + {/* Header */} @@ -46,15 +50,6 @@ export function EntityBrowser({ open, onOpenChange, screenId }: EntityBrowserPro Select items to add to your dashboard - {/* Content */} @@ -82,6 +77,6 @@ export function EntityBrowser({ open, onOpenChange, screenId }: EntityBrowserPro - + ) } diff --git a/src/components/__tests__/EntityBrowser.test.tsx b/src/components/__tests__/EntityBrowser.test.tsx index 4b02636..e7fb25c 100644 --- a/src/components/__tests__/EntityBrowser.test.tsx +++ b/src/components/__tests__/EntityBrowser.test.tsx @@ -274,20 +274,13 @@ describe('EntityBrowser', () => { expect(screen.getByText('No entities found')).toBeInTheDocument() }) - it('should handle cancel action', async () => { + it('should handle escape key to close', async () => { const user = userEvent.setup() render() - // The close button is the one with the Cross2Icon - it's a button without text - const buttons = screen.getAllByRole('button') - const closeButton = buttons.find((button) => { - // Find the button that contains the Cross2Icon (has no text content) - return button.querySelector('svg') && !button.textContent?.trim() - }) - - expect(closeButton).toBeTruthy() - await user.click(closeButton!) + // Press ESC key to close the drawer + await user.keyboard('{Escape}') expect(mockOnOpenChange).toHaveBeenCalledWith(false) }) diff --git a/src/components/ui/Drawer.tsx b/src/components/ui/Drawer.tsx new file mode 100644 index 0000000..e368173 --- /dev/null +++ b/src/components/ui/Drawer.tsx @@ -0,0 +1,136 @@ +import { forwardRef, type ReactNode } from 'react' +import * as Dialog from '@radix-ui/react-dialog' +import { Theme } from '@radix-ui/themes' +import { Cross2Icon } from '@radix-ui/react-icons' +import './drawer.css' + +type DrawerDirection = 'left' | 'right' | 'top' | 'bottom' + +interface DrawerProps { + /** + * Controls whether the drawer is open + */ + open: boolean + /** + * Callback when the drawer open state changes + */ + onOpenChange: (open: boolean) => void + /** + * Content to display in the drawer + */ + children: ReactNode + /** + * Direction from which the drawer slides in + * @default 'right' + */ + direction?: DrawerDirection + /** + * Whether to include the Theme wrapper. Default true for styled content. + * @default true + */ + includeTheme?: boolean + /** + * Whether clicking backdrop closes drawer. Default true. + * @default true + */ + closeOnBackdropClick?: boolean + /** + * Whether ESC key closes drawer. Default true. + * @default true + */ + closeOnEsc?: boolean + /** + * Custom width for left/right drawers or height for top/bottom drawers + */ + size?: string + /** + * Whether to show close button + * @default true + */ + showCloseButton?: boolean + /** + * Title for the drawer (for accessibility) + */ + title?: string + /** + * Description for the drawer (for accessibility) + */ + description?: string +} + +/** + * A drawer component built on Radix UI Dialog with CSS animations. + * Provides slide-in functionality from any edge of the viewport. + * + * Features: + * - Built on Radix Dialog for proper accessibility and focus management + * - CSS-based animations for test compatibility + * - Supports all four directions + * - Portal rendering to escape shadow DOM + * - ESC key and backdrop click support + */ +export const Drawer = forwardRef( + ( + { + open, + onOpenChange, + children, + direction = 'right', + includeTheme = true, + closeOnBackdropClick = true, + closeOnEsc = true, + size, + showCloseButton = true, + title, + description, + }, + ref + ) => { + const content = ( + + + onOpenChange(false) : undefined} + /> + e.preventDefault()} + onPointerDownOutside={closeOnBackdropClick ? undefined : (e) => e.preventDefault()} + onInteractOutside={closeOnBackdropClick ? undefined : (e) => e.preventDefault()} + > + {/* Always render title and description for accessibility, but hide them visually */} + {title || 'Dialog'} + + {description || 'Dialog content'} + + + {showCloseButton && ( + + + + )} + +
{children}
+
+
+
+ ) + + return includeTheme ? {content} : content + } +) + +Drawer.displayName = 'Drawer' diff --git a/src/components/ui/drawer.css b/src/components/ui/drawer.css new file mode 100644 index 0000000..ed9c549 --- /dev/null +++ b/src/components/ui/drawer.css @@ -0,0 +1,196 @@ +/* Drawer overlay */ +.drawer-overlay { + position: fixed; + inset: 0; + background-color: rgba(0, 0, 0, 0.5); + backdrop-filter: blur(4px); + animation: overlayShow 200ms cubic-bezier(0.16, 1, 0.3, 1); +} + +/* Base drawer content styles */ +.drawer-content { + position: fixed; + background-color: var(--color-panel-solid); + box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); + display: flex; + flex-direction: column; + overflow: hidden; +} + +/* Focus outline styling */ +.drawer-content:focus { + outline: none; +} + +/* Drawer body - scrollable content area */ +.drawer-body { + flex: 1; + overflow: auto; + -webkit-overflow-scrolling: touch; +} + +/* Close button */ +.drawer-close { + position: absolute; + top: 16px; + right: 16px; + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + background: transparent; + border: none; + cursor: pointer; + border-radius: var(--radius-2); + color: var(--gray-11); + transition: + background-color 200ms, + color 200ms; +} + +.drawer-close:hover { + background-color: var(--gray-a3); + color: var(--gray-12); +} + +.drawer-close:focus { + outline: 2px solid var(--accent-8); + outline-offset: 2px; +} + +/* Accessibility titles */ +.drawer-title { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border-width: 0; +} + +.drawer-description { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border-width: 0; +} + +/* Direction-specific styles */ + +/* Right drawer */ +.drawer-right { + top: 0; + right: 0; + bottom: 0; + width: 80vw; + max-width: 600px; + animation: slideInFromRight 300ms cubic-bezier(0.16, 1, 0.3, 1); +} + +/* Left drawer */ +.drawer-left { + top: 0; + left: 0; + bottom: 0; + width: 80vw; + max-width: 600px; + animation: slideInFromLeft 300ms cubic-bezier(0.16, 1, 0.3, 1); +} + +/* Top drawer */ +.drawer-top { + top: 0; + left: 0; + right: 0; + height: 80vh; + max-height: 600px; + animation: slideInFromTop 300ms cubic-bezier(0.16, 1, 0.3, 1); +} + +/* Bottom drawer */ +.drawer-bottom { + bottom: 0; + left: 0; + right: 0; + height: 80vh; + max-height: 600px; + animation: slideInFromBottom 300ms cubic-bezier(0.16, 1, 0.3, 1); +} + +/* Animations */ +@keyframes overlayShow { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes slideInFromRight { + from { + transform: translateX(100%); + } + to { + transform: translateX(0); + } +} + +@keyframes slideInFromLeft { + from { + transform: translateX(-100%); + } + to { + transform: translateX(0); + } +} + +@keyframes slideInFromTop { + from { + transform: translateY(-100%); + } + to { + transform: translateY(0); + } +} + +@keyframes slideInFromBottom { + from { + transform: translateY(100%); + } + to { + transform: translateY(0); + } +} + +/* Mobile adjustments */ +@media (max-width: 640px) { + .drawer-right, + .drawer-left { + width: 90vw; + max-width: none; + } + + .drawer-top, + .drawer-bottom { + height: 90vh; + max-height: none; + } +} + +/* Reduced motion support */ +@media (prefers-reduced-motion: reduce) { + .drawer-overlay, + .drawer-content { + animation-duration: 0.01ms !important; + } +} diff --git a/src/components/ui/index.ts b/src/components/ui/index.ts index 98effe4..994c728 100644 --- a/src/components/ui/index.ts +++ b/src/components/ui/index.ts @@ -5,6 +5,7 @@ export * from '@radix-ui/themes' export { Modal } from './Modal' export { AlertModal } from './AlertModal' export { FullscreenModal } from './FullscreenModal' +export { Drawer } from './Drawer' // Export skeleton component export { SkeletonCard } from './SkeletonCard' From 83c2ff151d7260a6326cd8003a19a4b5bb8c5dae Mon Sep 17 00:00:00 2001 From: Marian Rudzynski Date: Thu, 4 Dec 2025 00:20:33 +0000 Subject: [PATCH 02/11] fix: allow all hosts in Vite dev server for custom hostnames --- vite.config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/vite.config.ts b/vite.config.ts index 83b7878..ffbd0c2 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -133,6 +133,7 @@ function panelPlugin() { export default defineConfig({ server: { port: 3000, + allowedHosts: true, cors: { origin: '*', credentials: false, From a5aea4d35b4395b5a161d8151500a64136f98f8d Mon Sep 17 00:00:00 2001 From: Marian Rudzynski Date: Thu, 4 Dec 2025 00:25:43 +0000 Subject: [PATCH 03/11] fix: inherit theme appearance in Drawer portal content --- src/components/ui/Drawer.tsx | 94 +++++----- src/components/ui/__tests__/Drawer.test.tsx | 180 ++++++++++++++++++++ 2 files changed, 233 insertions(+), 41 deletions(-) create mode 100644 src/components/ui/__tests__/Drawer.test.tsx diff --git a/src/components/ui/Drawer.tsx b/src/components/ui/Drawer.tsx index e368173..fd138bc 100644 --- a/src/components/ui/Drawer.tsx +++ b/src/components/ui/Drawer.tsx @@ -1,6 +1,6 @@ -import { forwardRef, type ReactNode } from 'react' +import { forwardRef, type ReactNode, useContext } from 'react' import * as Dialog from '@radix-ui/react-dialog' -import { Theme } from '@radix-ui/themes' +import { Theme, ThemeContext } from '@radix-ui/themes' import { Cross2Icon } from '@radix-ui/react-icons' import './drawer.css' @@ -86,50 +86,62 @@ export const Drawer = forwardRef( }, ref ) => { - const content = ( - - - onOpenChange(false) : undefined} - /> - e.preventDefault()} - onPointerDownOutside={closeOnBackdropClick ? undefined : (e) => e.preventDefault()} - onInteractOutside={closeOnBackdropClick ? undefined : (e) => e.preventDefault()} - > - {/* Always render title and description for accessibility, but hide them visually */} - {title || 'Dialog'} - - {description || 'Dialog content'} - + // Get current theme context to inherit appearance in portal + // Use useContext directly to avoid throwing when not in a Theme + const themeContext = useContext(ThemeContext) + + const portalContent = ( + <> + onOpenChange(false) : undefined} + /> + e.preventDefault()} + onPointerDownOutside={closeOnBackdropClick ? undefined : (e) => e.preventDefault()} + onInteractOutside={closeOnBackdropClick ? undefined : (e) => e.preventDefault()} + > + {/* Always render title and description for accessibility, but hide them visually */} + {title || 'Dialog'} + + {description || 'Dialog content'} + + + {showCloseButton && ( + + + + )} - {showCloseButton && ( - - - - )} +
{children}
+
+ + ) -
{children}
-
+ return ( + + + {includeTheme ? ( + {portalContent} + ) : ( + portalContent + )} ) - - return includeTheme ? {content} : content } ) diff --git a/src/components/ui/__tests__/Drawer.test.tsx b/src/components/ui/__tests__/Drawer.test.tsx new file mode 100644 index 0000000..b4cbfcd --- /dev/null +++ b/src/components/ui/__tests__/Drawer.test.tsx @@ -0,0 +1,180 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { render, screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { Theme } from '@radix-ui/themes' +import { Drawer } from '../Drawer' + +describe('Drawer', () => { + const mockOnOpenChange = vi.fn() + + beforeEach(() => { + vi.clearAllMocks() + }) + + it('should render content when open', () => { + render( + +
Drawer Content
+
+ ) + + expect(screen.getByText('Drawer Content')).toBeInTheDocument() + }) + + it('should not render content when closed', () => { + render( + +
Drawer Content
+
+ ) + + expect(screen.queryByText('Drawer Content')).not.toBeInTheDocument() + }) + + it('should close on ESC key press', async () => { + const user = userEvent.setup() + + render( + +
Drawer Content
+
+ ) + + await user.keyboard('{Escape}') + + expect(mockOnOpenChange).toHaveBeenCalledWith(false) + }) + + it('should not close on ESC when closeOnEsc is false', async () => { + const user = userEvent.setup() + + render( + +
Drawer Content
+
+ ) + + await user.keyboard('{Escape}') + + expect(mockOnOpenChange).not.toHaveBeenCalled() + }) + + it('should show close button by default', () => { + render( + +
Drawer Content
+
+ ) + + expect(screen.getByRole('button', { name: 'Close' })).toBeInTheDocument() + }) + + it('should hide close button when showCloseButton is false', () => { + render( + +
Drawer Content
+
+ ) + + expect(screen.queryByRole('button', { name: 'Close' })).not.toBeInTheDocument() + }) + + it('should close when close button is clicked', async () => { + const user = userEvent.setup() + + render( + +
Drawer Content
+
+ ) + + await user.click(screen.getByRole('button', { name: 'Close' })) + + expect(mockOnOpenChange).toHaveBeenCalledWith(false) + }) + + it('should inherit dark theme appearance from parent Theme', () => { + render( + + +
Drawer Content
+
+
+ ) + + // The drawer content should be inside a Theme with dark appearance + const drawerContent = screen.getByTestId('drawer-content') + // Find the Theme wrapper in the portal - it should have the dark appearance class + const themeWrapper = drawerContent.closest('[data-is-root-theme]') + expect(themeWrapper).toHaveAttribute('data-is-root-theme', 'false') + // The drawer's Theme should have class indicating dark mode + expect(themeWrapper).toHaveClass('dark') + }) + + it('should inherit light theme appearance from parent Theme', () => { + render( + + +
Drawer Content
+
+
+ ) + + const drawerContent = screen.getByTestId('drawer-content') + const themeWrapper = drawerContent.closest('[data-is-root-theme]') + expect(themeWrapper).toHaveAttribute('data-is-root-theme', 'false') + expect(themeWrapper).toHaveClass('light') + }) + + it('should work without parent Theme (fallback)', () => { + // This should not throw - it should render with default theme + render( + +
Drawer Content
+
+ ) + + expect(screen.getByText('Drawer Content')).toBeInTheDocument() + }) + + it('should apply custom size', () => { + render( + +
Drawer Content
+
+ ) + + const drawerContent = screen.getByTestId('drawer-content') + const dialogContent = drawerContent.closest('.drawer-content') + expect(dialogContent).toHaveStyle({ width: '400px' }) + }) + + it('should apply direction class', () => { + render( + +
Drawer Content
+
+ ) + + const drawerContent = screen.getByTestId('drawer-content') + const dialogContent = drawerContent.closest('.drawer-content') + expect(dialogContent).toHaveClass('drawer-left') + }) + + it('should render accessibility title and description', () => { + render( + +
Drawer Content
+
+ ) + + // Title and description are visually hidden but present for screen readers + expect(screen.getByText('Test Title')).toBeInTheDocument() + expect(screen.getByText('Test Description')).toBeInTheDocument() + }) +}) From faf950c4671de71eef4217848b20f0b5e1239da0 Mon Sep 17 00:00:00 2001 From: Marian Rudzynski Date: Thu, 4 Dec 2025 00:40:03 +0000 Subject: [PATCH 04/11] feat(drawer): add partial overlay support to Drawer component --- src/components/ui/Drawer.tsx | 16 ++++++++++++++-- src/components/ui/drawer.css | 15 ++++++++++++--- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/components/ui/Drawer.tsx b/src/components/ui/Drawer.tsx index fd138bc..1fe8109 100644 --- a/src/components/ui/Drawer.tsx +++ b/src/components/ui/Drawer.tsx @@ -56,6 +56,16 @@ interface DrawerProps { * Description for the drawer (for accessibility) */ description?: string + /** + * Container element to portal the drawer into. If not provided, portals to document.body. + */ + portalContainer?: HTMLElement | null + /** + * Whether to use partial overlay (absolute positioning within container) + * instead of full-screen overlay (fixed positioning) + * @default false + */ + partialOverlay?: boolean } /** @@ -83,6 +93,8 @@ export const Drawer = forwardRef( showCloseButton = true, title, description, + portalContainer, + partialOverlay = false, }, ref ) => { @@ -93,7 +105,7 @@ export const Drawer = forwardRef( const portalContent = ( <> onOpenChange(false) : undefined} /> ( return ( - + {includeTheme ? ( {portalContent} ) : ( diff --git a/src/components/ui/drawer.css b/src/components/ui/drawer.css index ed9c549..045010c 100644 --- a/src/components/ui/drawer.css +++ b/src/components/ui/drawer.css @@ -7,6 +7,15 @@ animation: overlayShow 200ms cubic-bezier(0.16, 1, 0.3, 1); } +/* Partial overlay - only covers container, not full screen */ +.drawer-overlay-partial { + position: absolute; + inset: 0; + background-color: rgba(0, 0, 0, 0.5); + backdrop-filter: blur(2px); + animation: overlayShow 200ms cubic-bezier(0.16, 1, 0.3, 1); +} + /* Base drawer content styles */ .drawer-content { position: fixed; @@ -101,8 +110,8 @@ top: 0; left: 0; bottom: 0; - width: 80vw; - max-width: 600px; + width: 60vw; + max-width: 100vw; animation: slideInFromLeft 300ms cubic-bezier(0.16, 1, 0.3, 1); } @@ -177,7 +186,7 @@ .drawer-right, .drawer-left { width: 90vw; - max-width: none; + max-width: 100vw; } .drawer-top, From da62dbaa5d1ef20452102a09d4acc1953c7a3637 Mon Sep 17 00:00:00 2001 From: Marian Rudzynski Date: Thu, 4 Dec 2025 00:40:12 +0000 Subject: [PATCH 05/11] feat(drawer): change EntityBrowser drawer to left side with partial overlay --- src/components/Dashboard.tsx | 6 +++++- src/components/EntityBrowser.tsx | 14 +++++++++++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/components/Dashboard.tsx b/src/components/Dashboard.tsx index ead7f91..f4259b5 100644 --- a/src/components/Dashboard.tsx +++ b/src/components/Dashboard.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react' +import { useState, useEffect, useRef } from 'react' import { Box, Flex, Card, Text, Button } from '@radix-ui/themes' import { ScreenConfigDialog } from './ScreenConfigDialog' import { GridView } from './GridView' @@ -17,6 +17,7 @@ export function Dashboard() { const [addItemOpen, setAddItemOpen] = useState(false) const [addItemScreenId, setAddItemScreenId] = useState(null) const [editScreen, setEditScreen] = useState(undefined) + const mainContentRef = useRef(null) // Enable entity connection useEntityConnection() @@ -82,9 +83,11 @@ export function Dashboard() { {/* Content Area */} {currentScreen ? ( @@ -153,6 +156,7 @@ export function Dashboard() { } }} screenId={addItemScreenId} + portalContainer={mainContentRef.current} /> ) diff --git a/src/components/EntityBrowser.tsx b/src/components/EntityBrowser.tsx index 8bc88a6..f2dbb9a 100644 --- a/src/components/EntityBrowser.tsx +++ b/src/components/EntityBrowser.tsx @@ -8,9 +8,15 @@ interface EntityBrowserProps { open: boolean onOpenChange: (open: boolean) => void screenId: string | null + portalContainer?: HTMLElement | null } -export function EntityBrowser({ open, onOpenChange, screenId }: EntityBrowserProps) { +export function EntityBrowser({ + open, + onOpenChange, + screenId, + portalContainer, +}: EntityBrowserProps) { const handleClose = useCallback(() => { onOpenChange(false) }, [onOpenChange]) @@ -19,9 +25,11 @@ export function EntityBrowser({ open, onOpenChange, screenId }: EntityBrowserPro Date: Thu, 4 Dec 2025 00:41:09 +0000 Subject: [PATCH 06/11] chore: update auto-generated route tree types --- src/routeTree.gen.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index 3859325..ab26bfa 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -135,3 +135,12 @@ const rootRouteChildren: RootRouteChildren = { export const routeTree = rootRouteImport ._addFileChildren(rootRouteChildren) ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/react-start' +declare module '@tanstack/react-start' { + interface Register { + ssr: true + router: Awaited> + } +} From 48e56879f21f86a85ce986222be8a0617a805a59 Mon Sep 17 00:00:00 2001 From: Marian Rudzynski Date: Thu, 4 Dec 2025 00:57:07 +0000 Subject: [PATCH 07/11] fix(drawer): use full width on mobile for left drawer --- src/components/ui/drawer.css | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/components/ui/drawer.css b/src/components/ui/drawer.css index 045010c..daf6bf8 100644 --- a/src/components/ui/drawer.css +++ b/src/components/ui/drawer.css @@ -183,12 +183,16 @@ /* Mobile adjustments */ @media (max-width: 640px) { - .drawer-right, - .drawer-left { + .drawer-right { width: 90vw; max-width: 100vw; } + .drawer-left { + width: 100%; + max-width: 100%; + } + .drawer-top, .drawer-bottom { height: 90vh; From da4522a3095c386d0cd1eaee150d1e41ba25a1f3 Mon Sep 17 00:00:00 2001 From: Marian Rudzynski Date: Thu, 4 Dec 2025 00:59:56 +0000 Subject: [PATCH 08/11] fix(drawer): use absolute positioning for HA panel compatibility --- CLAUDE.md | 19 ++++++++++++++----- src/components/ui/drawer.css | 8 ++++---- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index e610284..64a150f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -384,24 +384,33 @@ When completing the final sub-issue of an epic, close the epic in the same pull - Built with vanilla CSS, no built-in `css` or `sx` props - Customize through props and theme configuration, NOT custom CSS -2. **Z-Index Management** +2. **🚨 CRITICAL: No Fixed Positioning 🚨** + - **NEVER use `position: fixed`** in any CSS or inline styles + - Liebe runs as a custom panel (web component) inside Home Assistant's UI + - Fixed positioning would position elements relative to Home Assistant's viewport, not Liebe's container + - This breaks the UI because elements would overlap Home Assistant's own sidebar/menu + - **ALWAYS use `position: absolute`** with a positioned parent container instead + - All overlays, modals, drawers, and floating elements must use absolute positioning + +3. **Z-Index Management** - **AVOID custom z-index values** - only use `auto`, `0`, or `-1` - Radix components that need stacking (modals, dropdowns) render in portals - Portalled components automatically manage stacking order without z-index conflicts - If you must set z-index (which you shouldn't), ensure it doesn't interfere with portal stacking -3. **Recommended Styling Approach** (in order of preference) +4. **Recommended Styling Approach** (in order of preference) 1. Use existing component props and theme configuration 2. Adjust the underlying token system (CSS variables) 3. Create custom components using Radix Primitives + Radix Colors 4. As a last resort, apply minimal style overrides -4. **What NOT to Do** +5. **What NOT to Do** - Don't extensively override component styles with custom CSS - Don't use arbitrary z-index values (like 99999 or 100000) + - Don't use `position: fixed` - always use `absolute` with a positioned container - Don't fight the design system - work with it -5. **Example: Fixing Dropdown Issues** +6. **Example: Fixing Dropdown Issues** Instead of: ```tsx @@ -417,7 +426,7 @@ When completing the final sub-issue of an epic, close the epic in the same pull // Content automatically renders in portal with proper stacking ``` -6. **Custom Components** +7. **Custom Components** When creating custom components, use: - Theme tokens for consistency - Radix Primitives for behavior diff --git a/src/components/ui/drawer.css b/src/components/ui/drawer.css index daf6bf8..f4d2ce7 100644 --- a/src/components/ui/drawer.css +++ b/src/components/ui/drawer.css @@ -1,6 +1,6 @@ -/* Drawer overlay */ +/* Drawer overlay - uses absolute positioning for Home Assistant panel compatibility */ .drawer-overlay { - position: fixed; + position: absolute; inset: 0; background-color: rgba(0, 0, 0, 0.5); backdrop-filter: blur(4px); @@ -16,9 +16,9 @@ animation: overlayShow 200ms cubic-bezier(0.16, 1, 0.3, 1); } -/* Base drawer content styles */ +/* Base drawer content styles - uses absolute positioning for Home Assistant panel compatibility */ .drawer-content { - position: fixed; + position: absolute; background-color: var(--color-panel-solid); box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); display: flex; From bc4a70054e4346b0d5b4c08b613824391b5c71a3 Mon Sep 17 00:00:00 2001 From: Marian Rudzynski Date: Thu, 4 Dec 2025 01:05:37 +0000 Subject: [PATCH 09/11] fix(drawer): left drawer fills container with no borders or radius --- src/components/ui/drawer.css | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/ui/drawer.css b/src/components/ui/drawer.css index f4d2ce7..4f80cdf 100644 --- a/src/components/ui/drawer.css +++ b/src/components/ui/drawer.css @@ -105,13 +105,16 @@ animation: slideInFromRight 300ms cubic-bezier(0.16, 1, 0.3, 1); } -/* Left drawer */ +/* Left drawer - fills container from left edge to right edge */ .drawer-left { top: 0; left: 0; + right: 0; bottom: 0; - width: 60vw; - max-width: 100vw; + width: auto; + max-width: none; + border-radius: 0; + border-left: none; animation: slideInFromLeft 300ms cubic-bezier(0.16, 1, 0.3, 1); } @@ -188,10 +191,7 @@ max-width: 100vw; } - .drawer-left { - width: 100%; - max-width: 100%; - } + /* Left drawer already fills container via inset, no changes needed */ .drawer-top, .drawer-bottom { From adb16275daea3c94316b691df6ebe5a35a83230d Mon Sep 17 00:00:00 2001 From: Marian Rudzynski Date: Thu, 4 Dec 2025 01:39:48 +0000 Subject: [PATCH 10/11] fix(drawer): remove size prop to let CSS control left drawer dimensions --- src/components/EntityBrowser.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/EntityBrowser.tsx b/src/components/EntityBrowser.tsx index f2dbb9a..bfbffd5 100644 --- a/src/components/EntityBrowser.tsx +++ b/src/components/EntityBrowser.tsx @@ -26,7 +26,6 @@ export function EntityBrowser({ open={open} onOpenChange={onOpenChange} direction="left" - size="60vw" showCloseButton={false} portalContainer={portalContainer} partialOverlay={true} From 0c888a416614a9b050a11d1fa04d784dc2fdbd05 Mon Sep 17 00:00:00 2001 From: Marian Rudzynski Date: Thu, 4 Dec 2025 03:23:25 +0000 Subject: [PATCH 11/11] fix(drawer): use 60% width on desktop, 100% on mobile for left drawer --- ...onnection-status-popover-2025-01-06-fixed.png | Bin 5494 -> 0 bytes ...status-popover-2025-01-06-properly-styled.png | Bin 5494 -> 0 bytes src/components/ui/drawer.css | 10 ++++++---- 3 files changed, 6 insertions(+), 4 deletions(-) delete mode 100644 screenshots/connection-status-popover-2025-01-06-fixed.png delete mode 100644 screenshots/connection-status-popover-2025-01-06-properly-styled.png diff --git a/screenshots/connection-status-popover-2025-01-06-fixed.png b/screenshots/connection-status-popover-2025-01-06-fixed.png deleted file mode 100644 index 901cb997ac29806056d5fb8e88857a4f367dc82f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5494 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 diff --git a/screenshots/connection-status-popover-2025-01-06-properly-styled.png b/screenshots/connection-status-popover-2025-01-06-properly-styled.png deleted file mode 100644 index 901cb997ac29806056d5fb8e88857a4f367dc82f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5494 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 diff --git a/src/components/ui/drawer.css b/src/components/ui/drawer.css index 4f80cdf..e485962 100644 --- a/src/components/ui/drawer.css +++ b/src/components/ui/drawer.css @@ -105,13 +105,12 @@ animation: slideInFromRight 300ms cubic-bezier(0.16, 1, 0.3, 1); } -/* Left drawer - fills container from left edge to right edge */ +/* Left drawer - 60% width on desktop, full width on mobile */ .drawer-left { top: 0; left: 0; - right: 0; bottom: 0; - width: auto; + width: 60%; max-width: none; border-radius: 0; border-left: none; @@ -191,7 +190,10 @@ max-width: 100vw; } - /* Left drawer already fills container via inset, no changes needed */ + /* Left drawer goes full width on mobile */ + .drawer-left { + width: 100%; + } .drawer-top, .drawer-bottom {