@zendir/ui 0.2.16 → 0.2.17

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.
@@ -128,7 +128,7 @@ const SidePanel = memo(function SidePanel2({
128
128
  display: "flex",
129
129
  alignItems: "center",
130
130
  justifyContent: "space-between",
131
- padding: `${tokens.spacing.sm} ${tokens.spacing.md}`,
131
+ padding: `${tokens.spacing.md} ${tokens.spacing.md}`,
132
132
  borderBottom: tokens.borders.divider,
133
133
  flexShrink: 0,
134
134
  gap: tokens.spacing.sm,
@@ -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 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 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.sm} ${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 }}\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 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 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 }}\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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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zendir/ui",
3
- "version": "0.2.16",
3
+ "version": "0.2.17",
4
4
  "description": "React UI components for space operations, built on the Astro UX Design System",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",