@zendir/ui 0.2.18 → 0.2.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# @zendir/ui
|
|
2
2
|
|
|
3
|
+
## 0.2.20
|
|
4
|
+
|
|
5
|
+
### Improvements
|
|
6
|
+
|
|
7
|
+
- **SidePanel**: Smoother open/close animation — CSS transitions increased from 0.3s to 0.5s with matched JavaScript timing (550ms closing timeout) to prevent premature unmount during slide-out.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
3
11
|
## 0.2.5
|
|
4
12
|
|
|
5
13
|
### Bug Fixes
|
|
@@ -32,12 +32,12 @@ const SidePanel = memo(function SidePanel2({
|
|
|
32
32
|
useEffect(() => {
|
|
33
33
|
if (open) {
|
|
34
34
|
setAnimState("opening");
|
|
35
|
-
const timer = setTimeout(() => setAnimState("open"),
|
|
35
|
+
const timer = setTimeout(() => setAnimState("open"), 30);
|
|
36
36
|
return () => clearTimeout(timer);
|
|
37
37
|
} else {
|
|
38
38
|
if (animState === "open" || animState === "opening") {
|
|
39
39
|
setAnimState("closing");
|
|
40
|
-
const timer = setTimeout(() => setAnimState("closed"),
|
|
40
|
+
const timer = setTimeout(() => setAnimState("closed"), 550);
|
|
41
41
|
return () => clearTimeout(timer);
|
|
42
42
|
}
|
|
43
43
|
}
|
|
@@ -86,7 +86,7 @@ const SidePanel = memo(function SidePanel2({
|
|
|
86
86
|
maxWidth: resolvedWidth,
|
|
87
87
|
height: "100%",
|
|
88
88
|
overflow: "hidden",
|
|
89
|
-
transition: "width 0.
|
|
89
|
+
transition: "width 0.5s cubic-bezier(0.4, 0, 0.2, 1), min-width 0.5s cubic-bezier(0.4, 0, 0.2, 1)",
|
|
90
90
|
flexShrink: 0,
|
|
91
91
|
position: "relative",
|
|
92
92
|
zIndex: overlay ? 1e3 : "auto",
|
|
@@ -109,7 +109,7 @@ const SidePanel = memo(function SidePanel2({
|
|
|
109
109
|
borderLeft: position === "right" ? tokens.borders.divider : void 0,
|
|
110
110
|
borderRight: position === "left" ? tokens.borders.divider : void 0,
|
|
111
111
|
transform: isVisible ? "translateX(0)" : slideFrom,
|
|
112
|
-
transition: "transform 0.
|
|
112
|
+
transition: "transform 0.5s cubic-bezier(0.4, 0, 0.2, 1)",
|
|
113
113
|
overflow: "hidden"
|
|
114
114
|
},
|
|
115
115
|
children: [
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SidePanel.js","sources":["../../../src/react/core/SidePanel.tsx"],"sourcesContent":["/**\r\n * @zendir/ui - SidePanel Component\r\n * \r\n * A sliding panel that pushes content instead of overlaying it.\r\n * Designed to be placed as a flex sibling so the parent layout\r\n * naturally adjusts when the panel opens/closes.\r\n * \r\n * Features:\r\n * - Push-content behavior (flex sibling, content adjusts automatically)\r\n * - Smooth slide-in/out animation\r\n * - Configurable width, position (left/right)\r\n * - Optional overlay backdrop (for modal-like behavior)\r\n * - Theme-integrated via useTheme()\r\n * - Accessible (focus trap, keyboard support)\r\n * - Close on escape key\r\n * \r\n * @example\r\n * ```tsx\r\n * // Push-content pattern: SidePanel as flex sibling\r\n * <div style={{ display: 'flex', height: '100vh' }}>\r\n * <main style={{ flex: 1, transition: 'all 0.3s ease' }}>\r\n * {children}\r\n * </main>\r\n * <SidePanel\r\n * open={isChatOpen}\r\n * onClose={() => setIsChatOpen(false)}\r\n * title=\"Chat Assistant\"\r\n * width={380}\r\n * >\r\n * <ChatPanel messages={messages} onSend={handleSend} />\r\n * </SidePanel>\r\n * </div>\r\n * ```\r\n */\r\n\r\nimport React, { memo, useEffect, useRef, useCallback, useState } from 'react';\r\nimport { useTheme } from '../theme';\r\nimport { classNames } from '../utils';\r\nimport { useBreakpoint } from './layout/useBreakpoint';\r\n\r\n// ============================================================================\r\n// Types\r\n// ============================================================================\r\n\r\nexport type SidePanelPosition = 'left' | 'right';\r\n\r\nexport interface SidePanelProps {\r\n /** Whether the panel is open */\r\n open: boolean;\r\n /** Called when the panel should close */\r\n onClose: () => void;\r\n /** Panel title displayed in the header */\r\n title?: string;\r\n /** Panel width in px or CSS string (default: 380) */\r\n width?: number | string;\r\n /** Which side the panel slides in from (default: 'right') */\r\n position?: SidePanelPosition;\r\n /** Show a close button in the header (default: true) */\r\n showCloseButton?: boolean;\r\n /** Close when pressing Escape (default: true) */\r\n closeOnEscape?: boolean;\r\n /** Show a semi-transparent overlay behind the panel (default: false) */\r\n overlay?: boolean;\r\n /** Close when clicking the overlay (default: true, only applies when overlay=true) */\r\n closeOnOverlayClick?: boolean;\r\n /** Panel content */\r\n children: React.ReactNode;\r\n /** Optional header actions (rendered in header, right of title) */\r\n headerActions?: React.ReactNode;\r\n /** Optional leading element (rendered in header, left of title — useful for collapse icons) */\r\n headerLeading?: React.ReactNode;\r\n /** Called when the header bar is clicked (useful for collapse-on-click) */\r\n onHeaderClick?: () => void;\r\n /** Custom style overrides for the header bar */\r\n headerStyle?: React.CSSProperties;\r\n /** Custom className */\r\n className?: string;\r\n /** Custom style overrides for the panel container */\r\n style?: React.CSSProperties;\r\n}\r\n\r\n// ============================================================================\r\n// Unique animation ID\r\n// ============================================================================\r\n\r\nlet sidePanelInstanceCount = 0;\r\n\r\n// ============================================================================\r\n// Component\r\n// ============================================================================\r\n\r\nexport const SidePanel = memo(function SidePanel({\r\n open,\r\n onClose,\r\n title,\r\n width = 380,\r\n position = 'right',\r\n showCloseButton = true,\r\n closeOnEscape = true,\r\n overlay = false,\r\n closeOnOverlayClick = true,\r\n children,\r\n headerActions,\r\n headerLeading,\r\n onHeaderClick,\r\n headerStyle: headerStyleProp,\r\n className = '',\r\n style: styleProp,\r\n}: SidePanelProps): React.ReactElement | null {\r\n const { tokens, theme } = useTheme();\r\n const isTransparentTheme = theme === 'transparent' || theme === 'transparent-bold' || theme === 'transparent-minimal';\r\n const panelRef = useRef<HTMLDivElement>(null);\r\n const [animState, setAnimState] = useState<'closed' | 'opening' | 'open' | 'closing'>('closed');\r\n const _instanceId = useRef(++sidePanelInstanceCount);\r\n\r\n const { isMobile } = useBreakpoint();\r\n const resolvedWidth = isMobile ? '100%' : (typeof width === 'number' ? `${width}px` : width);\r\n\r\n // Animate open/close\r\n useEffect(() => {\r\n if (open) {\r\n setAnimState('opening');\r\n const timer = setTimeout(() => setAnimState('open'), 20);\r\n return () => clearTimeout(timer);\r\n } else {\r\n if (animState === 'open' || animState === 'opening') {\r\n setAnimState('closing');\r\n const timer = setTimeout(() => setAnimState('closed'), 300);\r\n return () => clearTimeout(timer);\r\n }\r\n }\r\n }, [open]);\r\n\r\n // Escape key handler\r\n const handleKeyDown = useCallback((e: KeyboardEvent) => {\r\n if (closeOnEscape && e.key === 'Escape') {\r\n onClose();\r\n }\r\n }, [closeOnEscape, onClose]);\r\n\r\n useEffect(() => {\r\n if (open) {\r\n document.addEventListener('keydown', handleKeyDown);\r\n return () => document.removeEventListener('keydown', handleKeyDown);\r\n }\r\n }, [open, handleKeyDown]);\r\n\r\n // Don't render at all when fully closed\r\n if (animState === 'closed') return null;\r\n\r\n const isVisible = animState === 'open';\r\n const slideFrom = position === 'right' ? 'translateX(100%)' : 'translateX(-100%)';\r\n\r\n return (\r\n <>\r\n {/* Optional overlay */}\r\n {overlay && (\r\n <div\r\n className=\"zendir-sidepanel-overlay\"\r\n onClick={closeOnOverlayClick ? onClose : undefined}\r\n style={{\r\n position: 'fixed',\r\n inset: 0,\r\n backgroundColor: isTransparentTheme ? `${tokens.colors.background.base}4D` : `${tokens.colors.background.base}66`,\r\n backdropFilter: isTransparentTheme ? 'blur(2px)' : undefined,\r\n WebkitBackdropFilter: isTransparentTheme ? 'blur(2px)' : undefined,\r\n zIndex: 999,\r\n opacity: isVisible ? 1 : 0,\r\n transition: 'opacity 0.3s ease',\r\n pointerEvents: isVisible ? 'auto' : 'none',\r\n }}\r\n />\r\n )}\r\n\r\n {/* Panel */}\r\n <div\r\n ref={panelRef}\r\n className={classNames('zendir-sidepanel', className)}\r\n style={{\r\n width: isVisible || animState === 'opening' ? resolvedWidth : '0px',\r\n minWidth: isVisible || animState === 'opening' ? resolvedWidth : '0px',\r\n maxWidth: resolvedWidth,\r\n height: '100%',\r\n overflow: 'hidden',\r\n transition: 'width 0.3s cubic-bezier(0.4, 0, 0.2, 1), min-width 0.3s cubic-bezier(0.4, 0, 0.2, 1)',\r\n flexShrink: 0,\r\n position: 'relative',\r\n zIndex: overlay ? 1000 : 'auto',\r\n ...(overlay && { position: 'fixed', top: 0, [position]: 0 }),\r\n ...styleProp,\r\n }}\r\n >\r\n <div\r\n style={{\r\n width: resolvedWidth,\r\n height: '100%',\r\n display: 'flex',\r\n flexDirection: 'column',\r\n backgroundColor: isTransparentTheme\r\n ? 'rgba(15, 15, 35, 0.85)'\r\n : tokens.colors.background.surface,\r\n ...(isTransparentTheme && {\r\n backdropFilter: 'blur(16px)',\r\n WebkitBackdropFilter: 'blur(16px)',\r\n }),\r\n borderLeft: position === 'right' ? tokens.borders.divider : undefined,\r\n borderRight: position === 'left' ? tokens.borders.divider : undefined,\r\n transform: isVisible ? 'translateX(0)' : slideFrom,\r\n transition: 'transform 0.3s cubic-bezier(0.4, 0, 0.2, 1)',\r\n overflow: 'hidden',\r\n }}\r\n >\r\n {/* Header */}\r\n {(title || showCloseButton || headerActions || headerLeading) && (\r\n <div\r\n onClick={onHeaderClick}\r\n role={onHeaderClick ? 'button' : undefined}\r\n tabIndex={onHeaderClick ? 0 : undefined}\r\n onKeyDown={onHeaderClick ? (e: React.KeyboardEvent) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onHeaderClick(); } } : undefined}\r\n style={{\r\n display: 'flex',\r\n alignItems: 'center',\r\n justifyContent: 'space-between',\r\n padding: `${tokens.spacing.md} ${tokens.spacing.md}`,\r\n borderBottom: tokens.borders.divider,\r\n flexShrink: 0,\r\n gap: tokens.spacing.sm,\r\n ...(onHeaderClick && { cursor: 'pointer' }),\r\n ...headerStyleProp,\r\n }}\r\n >\r\n <div style={{ display: 'flex', alignItems: 'center', gap: tokens.spacing.sm, flex: 1, minWidth: 0 }}>\r\n {headerLeading}\r\n {title && (\r\n <h3\r\n style={{\r\n margin: 0,\r\n fontSize: tokens.typography.fontSize.base,\r\n fontWeight: tokens.typography.fontWeight.semibold,\r\n color: tokens.colors.text.primary,\r\n whiteSpace: 'nowrap',\r\n overflow: 'hidden',\r\n textOverflow: 'ellipsis',\r\n }}\r\n >\r\n {title}\r\n </h3>\r\n )}\r\n </div>\r\n <div style={{ display: 'flex', alignItems: 'center', gap: tokens.spacing.xs, flexShrink: 0 }}>\r\n {headerActions}\r\n {showCloseButton && (\r\n <button\r\n type=\"button\"\r\n onClick={onClose}\r\n aria-label=\"Close panel\"\r\n style={{\r\n padding: tokens.spacing.sm,\r\n minWidth: 44,\r\n minHeight: 44,\r\n backgroundColor: 'transparent',\r\n border: 'none',\r\n borderRadius: tokens.borderRadius.sm,\r\n cursor: 'pointer',\r\n color: tokens.colors.text.muted ?? tokens.colors.text.tertiary,\r\n display: 'flex',\r\n alignItems: 'center',\r\n justifyContent: 'center',\r\n transition: `color 0.15s ease, background-color 0.15s ease`,\r\n }}\r\n onMouseEnter={(e) => {\r\n e.currentTarget.style.color = tokens.colors.text.primary;\r\n e.currentTarget.style.backgroundColor = tokens.colors.background.base;\r\n }}\r\n onMouseLeave={(e) => {\r\n e.currentTarget.style.color = tokens.colors.text.muted ?? tokens.colors.text.tertiary;\r\n e.currentTarget.style.backgroundColor = 'transparent';\r\n }}\r\n >\r\n <svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\r\n <path d=\"M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z\" />\r\n </svg>\r\n </button>\r\n )}\r\n </div>\r\n </div>\r\n )}\r\n\r\n {/* Content */}\r\n <div\r\n style={{\r\n flex: 1,\r\n overflow: 'auto',\r\n minHeight: 0,\r\n }}\r\n >\r\n {children}\r\n </div>\r\n </div>\r\n </div>\r\n </>\r\n );\r\n});\r\n\r\nexport default SidePanel;\r\n"],"names":["SidePanel"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"SidePanel.js","sources":["../../../src/react/core/SidePanel.tsx"],"sourcesContent":["/**\r\n * @zendir/ui - SidePanel Component\r\n * \r\n * A sliding panel that pushes content instead of overlaying it.\r\n * Designed to be placed as a flex sibling so the parent layout\r\n * naturally adjusts when the panel opens/closes.\r\n * \r\n * Features:\r\n * - Push-content behavior (flex sibling, content adjusts automatically)\r\n * - Smooth slide-in/out animation\r\n * - Configurable width, position (left/right)\r\n * - Optional overlay backdrop (for modal-like behavior)\r\n * - Theme-integrated via useTheme()\r\n * - Accessible (focus trap, keyboard support)\r\n * - Close on escape key\r\n * \r\n * @example\r\n * ```tsx\r\n * // Push-content pattern: SidePanel as flex sibling\r\n * <div style={{ display: 'flex', height: '100vh' }}>\r\n * <main style={{ flex: 1, transition: 'all 0.3s ease' }}>\r\n * {children}\r\n * </main>\r\n * <SidePanel\r\n * open={isChatOpen}\r\n * onClose={() => setIsChatOpen(false)}\r\n * title=\"Chat Assistant\"\r\n * width={380}\r\n * >\r\n * <ChatPanel messages={messages} onSend={handleSend} />\r\n * </SidePanel>\r\n * </div>\r\n * ```\r\n */\r\n\r\nimport React, { memo, useEffect, useRef, useCallback, useState } from 'react';\r\nimport { useTheme } from '../theme';\r\nimport { classNames } from '../utils';\r\nimport { useBreakpoint } from './layout/useBreakpoint';\r\n\r\n// ============================================================================\r\n// Types\r\n// ============================================================================\r\n\r\nexport type SidePanelPosition = 'left' | 'right';\r\n\r\nexport interface SidePanelProps {\r\n /** Whether the panel is open */\r\n open: boolean;\r\n /** Called when the panel should close */\r\n onClose: () => void;\r\n /** Panel title displayed in the header */\r\n title?: string;\r\n /** Panel width in px or CSS string (default: 380) */\r\n width?: number | string;\r\n /** Which side the panel slides in from (default: 'right') */\r\n position?: SidePanelPosition;\r\n /** Show a close button in the header (default: true) */\r\n showCloseButton?: boolean;\r\n /** Close when pressing Escape (default: true) */\r\n closeOnEscape?: boolean;\r\n /** Show a semi-transparent overlay behind the panel (default: false) */\r\n overlay?: boolean;\r\n /** Close when clicking the overlay (default: true, only applies when overlay=true) */\r\n closeOnOverlayClick?: boolean;\r\n /** Panel content */\r\n children: React.ReactNode;\r\n /** Optional header actions (rendered in header, right of title) */\r\n headerActions?: React.ReactNode;\r\n /** Optional leading element (rendered in header, left of title — useful for collapse icons) */\r\n headerLeading?: React.ReactNode;\r\n /** Called when the header bar is clicked (useful for collapse-on-click) */\r\n onHeaderClick?: () => void;\r\n /** Custom style overrides for the header bar */\r\n headerStyle?: React.CSSProperties;\r\n /** Custom className */\r\n className?: string;\r\n /** Custom style overrides for the panel container */\r\n style?: React.CSSProperties;\r\n}\r\n\r\n// ============================================================================\r\n// Unique animation ID\r\n// ============================================================================\r\n\r\nlet sidePanelInstanceCount = 0;\r\n\r\n// ============================================================================\r\n// Component\r\n// ============================================================================\r\n\r\nexport const SidePanel = memo(function SidePanel({\r\n open,\r\n onClose,\r\n title,\r\n width = 380,\r\n position = 'right',\r\n showCloseButton = true,\r\n closeOnEscape = true,\r\n overlay = false,\r\n closeOnOverlayClick = true,\r\n children,\r\n headerActions,\r\n headerLeading,\r\n onHeaderClick,\r\n headerStyle: headerStyleProp,\r\n className = '',\r\n style: styleProp,\r\n}: SidePanelProps): React.ReactElement | null {\r\n const { tokens, theme } = useTheme();\r\n const isTransparentTheme = theme === 'transparent' || theme === 'transparent-bold' || theme === 'transparent-minimal';\r\n const panelRef = useRef<HTMLDivElement>(null);\r\n const [animState, setAnimState] = useState<'closed' | 'opening' | 'open' | 'closing'>('closed');\r\n const _instanceId = useRef(++sidePanelInstanceCount);\r\n\r\n const { isMobile } = useBreakpoint();\r\n const resolvedWidth = isMobile ? '100%' : (typeof width === 'number' ? `${width}px` : width);\r\n\r\n // Animate open/close\r\n useEffect(() => {\r\n if (open) {\r\n setAnimState('opening');\r\n const timer = setTimeout(() => setAnimState('open'), 30);\r\n return () => clearTimeout(timer);\r\n } else {\r\n if (animState === 'open' || animState === 'opening') {\r\n setAnimState('closing');\r\n const timer = setTimeout(() => setAnimState('closed'), 550);\r\n return () => clearTimeout(timer);\r\n }\r\n }\r\n }, [open]);\r\n\r\n // Escape key handler\r\n const handleKeyDown = useCallback((e: KeyboardEvent) => {\r\n if (closeOnEscape && e.key === 'Escape') {\r\n onClose();\r\n }\r\n }, [closeOnEscape, onClose]);\r\n\r\n useEffect(() => {\r\n if (open) {\r\n document.addEventListener('keydown', handleKeyDown);\r\n return () => document.removeEventListener('keydown', handleKeyDown);\r\n }\r\n }, [open, handleKeyDown]);\r\n\r\n // Don't render at all when fully closed\r\n if (animState === 'closed') return null;\r\n\r\n const isVisible = animState === 'open';\r\n const slideFrom = position === 'right' ? 'translateX(100%)' : 'translateX(-100%)';\r\n\r\n return (\r\n <>\r\n {/* Optional overlay */}\r\n {overlay && (\r\n <div\r\n className=\"zendir-sidepanel-overlay\"\r\n onClick={closeOnOverlayClick ? onClose : undefined}\r\n style={{\r\n position: 'fixed',\r\n inset: 0,\r\n backgroundColor: isTransparentTheme ? `${tokens.colors.background.base}4D` : `${tokens.colors.background.base}66`,\r\n backdropFilter: isTransparentTheme ? 'blur(2px)' : undefined,\r\n WebkitBackdropFilter: isTransparentTheme ? 'blur(2px)' : undefined,\r\n zIndex: 999,\r\n opacity: isVisible ? 1 : 0,\r\n transition: 'opacity 0.3s ease',\r\n pointerEvents: isVisible ? 'auto' : 'none',\r\n }}\r\n />\r\n )}\r\n\r\n {/* Panel */}\r\n <div\r\n ref={panelRef}\r\n className={classNames('zendir-sidepanel', className)}\r\n style={{\r\n width: isVisible || animState === 'opening' ? resolvedWidth : '0px',\r\n minWidth: isVisible || animState === 'opening' ? resolvedWidth : '0px',\r\n maxWidth: resolvedWidth,\r\n height: '100%',\r\n overflow: 'hidden',\r\n transition: 'width 0.5s cubic-bezier(0.4, 0, 0.2, 1), min-width 0.5s cubic-bezier(0.4, 0, 0.2, 1)',\r\n flexShrink: 0,\r\n position: 'relative',\r\n zIndex: overlay ? 1000 : 'auto',\r\n ...(overlay && { position: 'fixed', top: 0, [position]: 0 }),\r\n ...styleProp,\r\n }}\r\n >\r\n <div\r\n style={{\r\n width: resolvedWidth,\r\n height: '100%',\r\n display: 'flex',\r\n flexDirection: 'column',\r\n backgroundColor: isTransparentTheme\r\n ? 'rgba(15, 15, 35, 0.85)'\r\n : tokens.colors.background.surface,\r\n ...(isTransparentTheme && {\r\n backdropFilter: 'blur(16px)',\r\n WebkitBackdropFilter: 'blur(16px)',\r\n }),\r\n borderLeft: position === 'right' ? tokens.borders.divider : undefined,\r\n borderRight: position === 'left' ? tokens.borders.divider : undefined,\r\n transform: isVisible ? 'translateX(0)' : slideFrom,\r\n transition: 'transform 0.5s cubic-bezier(0.4, 0, 0.2, 1)',\r\n overflow: 'hidden',\r\n }}\r\n >\r\n {/* Header */}\r\n {(title || showCloseButton || headerActions || headerLeading) && (\r\n <div\r\n onClick={onHeaderClick}\r\n role={onHeaderClick ? 'button' : undefined}\r\n tabIndex={onHeaderClick ? 0 : undefined}\r\n onKeyDown={onHeaderClick ? (e: React.KeyboardEvent) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onHeaderClick(); } } : undefined}\r\n style={{\r\n display: 'flex',\r\n alignItems: 'center',\r\n justifyContent: 'space-between',\r\n padding: `${tokens.spacing.md} ${tokens.spacing.md}`,\r\n borderBottom: tokens.borders.divider,\r\n flexShrink: 0,\r\n gap: tokens.spacing.sm,\r\n ...(onHeaderClick && { cursor: 'pointer' }),\r\n ...headerStyleProp,\r\n }}\r\n >\r\n <div style={{ display: 'flex', alignItems: 'center', gap: tokens.spacing.sm, flex: 1, minWidth: 0 }}>\r\n {headerLeading}\r\n {title && (\r\n <h3\r\n style={{\r\n margin: 0,\r\n fontSize: tokens.typography.fontSize.base,\r\n fontWeight: tokens.typography.fontWeight.semibold,\r\n color: tokens.colors.text.primary,\r\n whiteSpace: 'nowrap',\r\n overflow: 'hidden',\r\n textOverflow: 'ellipsis',\r\n }}\r\n >\r\n {title}\r\n </h3>\r\n )}\r\n </div>\r\n <div style={{ display: 'flex', alignItems: 'center', gap: tokens.spacing.xs, flexShrink: 0 }}>\r\n {headerActions}\r\n {showCloseButton && (\r\n <button\r\n type=\"button\"\r\n onClick={onClose}\r\n aria-label=\"Close panel\"\r\n style={{\r\n padding: tokens.spacing.sm,\r\n minWidth: 44,\r\n minHeight: 44,\r\n backgroundColor: 'transparent',\r\n border: 'none',\r\n borderRadius: tokens.borderRadius.sm,\r\n cursor: 'pointer',\r\n color: tokens.colors.text.muted ?? tokens.colors.text.tertiary,\r\n display: 'flex',\r\n alignItems: 'center',\r\n justifyContent: 'center',\r\n transition: `color 0.15s ease, background-color 0.15s ease`,\r\n }}\r\n onMouseEnter={(e) => {\r\n e.currentTarget.style.color = tokens.colors.text.primary;\r\n e.currentTarget.style.backgroundColor = tokens.colors.background.base;\r\n }}\r\n onMouseLeave={(e) => {\r\n e.currentTarget.style.color = tokens.colors.text.muted ?? tokens.colors.text.tertiary;\r\n e.currentTarget.style.backgroundColor = 'transparent';\r\n }}\r\n >\r\n <svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\r\n <path d=\"M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z\" />\r\n </svg>\r\n </button>\r\n )}\r\n </div>\r\n </div>\r\n )}\r\n\r\n {/* Content */}\r\n <div\r\n style={{\r\n flex: 1,\r\n overflow: 'auto',\r\n minHeight: 0,\r\n }}\r\n >\r\n {children}\r\n </div>\r\n </div>\r\n </div>\r\n </>\r\n );\r\n});\r\n\r\nexport default SidePanel;\r\n"],"names":["SidePanel"],"mappings":"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"}
|