UNPKG

analytica-frontend-lib

Version:

Repositório público dos componentes utilizados nas plataformas da Analytica Ensino

1 lines 100 kB
{"version":3,"sources":["../../src/components/Search/Search.tsx","../../src/components/DropdownMenu/DropdownMenu.tsx","../../src/utils/utils.ts","../../src/components/Button/Button.tsx","../../src/components/Text/Text.tsx","../../src/components/Modal/Modal.tsx","../../src/components/Modal/utils/videoUtils.ts","../../src/components/ThemeToggle/ThemeToggle.tsx","../../src/components/SelectionButton/SelectionButton.tsx","../../src/hooks/useTheme.ts","../../src/store/themeStore.ts"],"sourcesContent":["import { X, MagnifyingGlass } from 'phosphor-react';\nimport {\n InputHTMLAttributes,\n forwardRef,\n useState,\n useId,\n useMemo,\n useEffect,\n useRef,\n ChangeEvent,\n MouseEvent,\n KeyboardEvent,\n} from 'react';\nimport DropdownMenu, {\n DropdownMenuContent,\n DropdownMenuItem,\n createDropdownStore,\n} from '../DropdownMenu/DropdownMenu';\n\n/**\n * Search component props interface\n */\ntype SearchProps = {\n /** List of options to show in dropdown */\n options: string[];\n /** Callback when an option is selected from dropdown */\n onSelect?: (value: string) => void;\n /** Callback when search input changes */\n onSearch?: (query: string) => void;\n /** Control dropdown visibility externally */\n showDropdown?: boolean;\n /** Callback when dropdown open state changes */\n onDropdownChange?: (open: boolean) => void;\n /** Maximum height of dropdown in pixels */\n dropdownMaxHeight?: number;\n /** Text to show when no results are found */\n noResultsText?: string;\n /** Additional CSS classes to apply to the input */\n className?: string;\n /** Additional CSS classes to apply to the container */\n containerClassName?: string;\n /** Callback when clear button is clicked */\n onClear?: () => void;\n} & Omit<InputHTMLAttributes<HTMLInputElement>, 'size' | 'onSelect'>;\n\n/**\n * Search component for Analytica Ensino platforms\n *\n * A specialized search input component with dropdown suggestions.\n * Features filtering, keyboard navigation, and customizable options.\n *\n * @param options - Array of search options to display in dropdown\n * @param onSelect - Callback when an option is selected\n * @param onSearch - Callback when search query changes\n * @param placeholder - Placeholder text for the input\n * @param noResultsText - Text to show when no results are found\n * @param dropdownMaxHeight - Maximum height of dropdown in pixels\n * @param className - Additional CSS classes for the input\n * @param containerClassName - Additional CSS classes for the container\n * @param props - All other standard input HTML attributes\n * @returns A styled search input with dropdown functionality\n *\n * @example\n * ```tsx\n * // Basic search\n * <Search\n * options={['Filosofia', 'Física', 'Matemática']}\n * placeholder=\"Buscar matéria...\"\n * onSelect={(value) => console.log('Selected:', value)}\n * />\n *\n * // With custom filtering\n * <Search\n * options={materias}\n * onSearch={(query) => setFilteredMaterias(filterMaterias(query))}\n * noResultsText=\"Nenhum resultado encontrado\"\n * />\n * ```\n */\n\n/**\n * Filter options based on search query\n */\nconst filterOptions = (options: string[], query: string): string[] => {\n if (!query || query.length < 1) return [];\n\n return options.filter((option) =>\n option.toLowerCase().includes(query.toLowerCase())\n );\n};\n\n/**\n * Updates input value and creates appropriate change event\n */\nconst updateInputValue = (\n value: string,\n ref:\n | { current: HTMLInputElement | null }\n | ((instance: HTMLInputElement | null) => void)\n | null,\n onChange?: (event: ChangeEvent<HTMLInputElement>) => void\n) => {\n if (!onChange) return;\n\n if (ref && 'current' in ref && ref.current) {\n ref.current.value = value;\n const event = new Event('input', { bubbles: true });\n Object.defineProperty(event, 'target', {\n writable: false,\n value: ref.current,\n });\n onChange(event as unknown as ChangeEvent<HTMLInputElement>);\n } else {\n // Fallback for cases where ref is not available\n const event = {\n target: { value },\n currentTarget: { value },\n } as ChangeEvent<HTMLInputElement>;\n onChange(event);\n }\n};\n\nconst Search = forwardRef<HTMLInputElement, SearchProps>(\n (\n {\n options = [],\n onSelect,\n onSearch,\n showDropdown: controlledShowDropdown,\n onDropdownChange,\n dropdownMaxHeight = 240,\n noResultsText = 'Nenhum resultado encontrado',\n className = '',\n containerClassName = '',\n disabled,\n readOnly,\n id,\n onClear,\n value,\n onChange,\n placeholder = 'Buscar...',\n onKeyDown: userOnKeyDown,\n ...props\n },\n ref\n ) => {\n // Dropdown state and logic\n const [dropdownOpen, setDropdownOpen] = useState(false);\n const [forceClose, setForceClose] = useState(false);\n const justSelectedRef = useRef(false);\n const dropdownStore = useRef(createDropdownStore()).current;\n const dropdownRef = useRef<HTMLDivElement>(null);\n const inputElRef = useRef<HTMLInputElement>(null);\n\n // Filter options based on input value\n const filteredOptions = useMemo(() => {\n if (!options.length) {\n return [];\n }\n const filtered = filterOptions(options, (value as string) || '');\n return filtered;\n }, [options, value]);\n\n // Control dropdown visibility\n const showDropdown =\n !forceClose &&\n (controlledShowDropdown ??\n (dropdownOpen && value && String(value).length > 0));\n\n // Helper to keep all consumers in sync\n const setOpenAndNotify = (open: boolean) => {\n setDropdownOpen(open);\n dropdownStore.setState({ open });\n onDropdownChange?.(open);\n };\n\n // Handle dropdown visibility changes\n useEffect(() => {\n // Don't reopen dropdown if we just selected an option\n if (justSelectedRef.current) {\n justSelectedRef.current = false;\n return;\n }\n // Respect forceClose even if value is non-empty\n if (forceClose) {\n setOpenAndNotify(false);\n return;\n }\n\n const shouldShow = Boolean(value && String(value).length > 0);\n setOpenAndNotify(shouldShow);\n }, [value, forceClose, onDropdownChange, dropdownStore]);\n\n // Handle option selection\n const handleSelectOption = (option: string) => {\n justSelectedRef.current = true; // Prevent immediate dropdown reopen\n setForceClose(true); // Force dropdown to close immediately\n onSelect?.(option);\n setOpenAndNotify(false);\n\n // Update input value if onChange is provided\n updateInputValue(option, ref, onChange);\n };\n\n // Handle click outside dropdown\n useEffect(() => {\n const handleClickOutside = (event: globalThis.MouseEvent) => {\n if (\n dropdownRef.current &&\n !dropdownRef.current.contains(event.target as Node)\n ) {\n setOpenAndNotify(false);\n }\n };\n\n if (showDropdown) {\n document.addEventListener('mousedown', handleClickOutside);\n }\n\n return () => {\n document.removeEventListener('mousedown', handleClickOutside);\n };\n }, [showDropdown, dropdownStore, onDropdownChange]);\n\n // Generate unique ID if not provided\n const generatedId = useId();\n const inputId = id ?? `search-${generatedId}`;\n const dropdownId = `${inputId}-dropdown`;\n\n // Handle clear button\n const handleClear = () => {\n if (onClear) {\n onClear();\n } else {\n updateInputValue('', ref, onChange);\n }\n };\n\n // Handle clear button click - mantém foco no input\n const handleClearClick = (e: MouseEvent) => {\n e.preventDefault(); // Evita que o input perca foco\n e.stopPropagation(); // Para propagação do evento\n handleClear();\n };\n\n // Handle search icon click - focus on input\n const handleSearchIconClick = (e: MouseEvent) => {\n e.preventDefault();\n e.stopPropagation();\n setTimeout(() => {\n inputElRef.current?.focus();\n }, 0);\n };\n\n // Handle input change\n const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {\n setForceClose(false); // Allow dropdown to open when user types\n onChange?.(e);\n onSearch?.(e.target.value);\n };\n\n // Handle keyboard events\n const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {\n // Let consumer run first; if they prevent default, skip our logic\n userOnKeyDown?.(e);\n if (e.defaultPrevented) return;\n\n if (e.key === 'Enter') {\n e.preventDefault();\n\n // If dropdown is open and there are filtered options, select the first one\n if (showDropdown && filteredOptions.length > 0) {\n handleSelectOption(filteredOptions[0]);\n } else if (value) {\n // If no dropdown or no options, execute search\n onSearch?.(String(value));\n setForceClose(true);\n setOpenAndNotify(false);\n }\n }\n };\n\n // Helper function for input state classes\n const getInputStateClasses = (disabled?: boolean, readOnly?: boolean) => {\n if (disabled) return 'cursor-not-allowed opacity-40';\n if (readOnly) return 'cursor-default focus:outline-none !text-text-900';\n return 'hover:border-border-400';\n };\n\n // Determine which icon to show\n const hasValue = String(value ?? '').length > 0;\n const showClearButton = hasValue && !disabled && !readOnly;\n const showSearchIcon = !hasValue && !disabled && !readOnly;\n\n return (\n <div\n ref={dropdownRef}\n className={`w-full max-w-lg md:w-[488px] ${containerClassName}`}\n >\n {/* Search Input Container */}\n <div className=\"relative flex items-center\">\n {/* Search Input Field */}\n <input\n ref={(node) => {\n // Forward to parent\n if (ref) {\n if (typeof ref === 'function') ref(node);\n else\n (ref as { current: HTMLInputElement | null }).current = node;\n }\n // Keep our own handle\n inputElRef.current = node;\n }}\n id={inputId}\n type=\"text\"\n className={`w-full py-0 px-4 pr-10 font-normal text-text-900 focus:outline-primary-950 border rounded-full bg-background focus:bg-primary-50 border-border-300 focus:border-2 focus:border-primary-950 h-10 placeholder:text-text-600 ${getInputStateClasses(disabled, readOnly)} ${className}`}\n value={value}\n onChange={handleInputChange}\n onKeyDown={handleKeyDown}\n disabled={disabled}\n readOnly={readOnly}\n placeholder={placeholder}\n aria-expanded={showDropdown ? 'true' : undefined}\n aria-haspopup={options.length > 0 ? 'listbox' : undefined}\n aria-controls={showDropdown ? dropdownId : undefined}\n aria-autocomplete=\"list\"\n role={options.length > 0 ? 'combobox' : undefined}\n {...props}\n />\n\n {/* Right Icon - Clear Button */}\n {showClearButton && (\n <div className=\"absolute right-3 top-1/2 transform -translate-y-1/2\">\n <button\n type=\"button\"\n className=\"p-0 border-0 bg-transparent cursor-pointer\"\n onMouseDown={handleClearClick}\n aria-label=\"Limpar busca\"\n >\n <span className=\"w-6 h-6 text-text-800 flex items-center justify-center hover:text-text-600 transition-colors\">\n <X />\n </span>\n </button>\n </div>\n )}\n\n {/* Right Icon - Search Icon */}\n {showSearchIcon && (\n <div className=\"absolute right-3 top-1/2 transform -translate-y-1/2\">\n <button\n type=\"button\"\n className=\"p-0 border-0 bg-transparent cursor-pointer\"\n onMouseDown={handleSearchIconClick}\n aria-label=\"Buscar\"\n >\n <span className=\"w-6 h-6 text-text-800 flex items-center justify-center hover:text-text-600 transition-colors\">\n <MagnifyingGlass />\n </span>\n </button>\n </div>\n )}\n </div>\n\n {/* Search Dropdown */}\n {showDropdown && (\n <DropdownMenu open={showDropdown} onOpenChange={setDropdownOpen}>\n <DropdownMenuContent\n id={dropdownId}\n className=\"w-full mt-1\"\n style={{ maxHeight: dropdownMaxHeight }}\n align=\"start\"\n >\n {filteredOptions.length > 0 ? (\n filteredOptions.map((option) => (\n <DropdownMenuItem\n key={option}\n onClick={() => handleSelectOption(option)}\n className=\"text-text-700 text-base leading-6 cursor-pointer\"\n >\n {option}\n </DropdownMenuItem>\n ))\n ) : (\n <div className=\"px-3 py-3 text-text-700 text-base\">\n {noResultsText}\n </div>\n )}\n </DropdownMenuContent>\n </DropdownMenu>\n )}\n </div>\n );\n }\n);\n\nSearch.displayName = 'Search';\n\nexport default Search;\n","import { CaretRight, SignOut, User } from 'phosphor-react';\nimport {\n forwardRef,\n ReactNode,\n ButtonHTMLAttributes,\n useEffect,\n useLayoutEffect,\n useRef,\n HTMLAttributes,\n MouseEvent,\n KeyboardEvent,\n isValidElement,\n Children,\n cloneElement,\n ReactElement,\n useState,\n RefObject,\n CSSProperties,\n} from 'react';\nimport { createPortal } from 'react-dom';\nimport { create, StoreApi, useStore } from 'zustand';\nimport Button from '../Button/Button';\nimport Text from '../Text/Text';\nimport { cn } from '../../utils/utils';\nimport Modal from '../Modal/Modal';\nimport { ThemeToggle } from '../ThemeToggle/ThemeToggle';\nimport type { ThemeMode } from '@/hooks/useTheme';\nimport { useTheme } from '../../hooks/useTheme';\n\ninterface DropdownStore {\n open: boolean;\n setOpen: (open: boolean) => void;\n}\n\ntype DropdownStoreApi = StoreApi<DropdownStore>;\n\nexport function createDropdownStore(): DropdownStoreApi {\n return create<DropdownStore>((set) => ({\n open: false,\n setOpen: (open) => set({ open }),\n }));\n}\n\nexport const useDropdownStore = (externalStore?: DropdownStoreApi) => {\n if (!externalStore) {\n throw new Error(\n 'Component must be used within a DropdownMenu (store is missing)'\n );\n }\n\n return externalStore;\n};\n\nconst injectStore = (\n children: ReactNode,\n store: DropdownStoreApi\n): ReactNode => {\n return Children.map(children, (child) => {\n if (isValidElement(child)) {\n const typedChild = child as ReactElement<{\n store?: DropdownStoreApi;\n children?: ReactNode;\n }>;\n\n // Aqui tu checa o displayName do componente\n const displayName = (\n typedChild.type as unknown as { displayName: string }\n ).displayName;\n\n // Lista de componentes que devem receber o store\n const allowed = [\n 'DropdownMenuTrigger',\n 'DropdownContent',\n 'DropdownMenuContent',\n 'DropdownMenuSeparator',\n 'DropdownMenuItem',\n 'MenuLabel',\n 'ProfileMenuTrigger',\n 'ProfileMenuHeader',\n 'ProfileMenuFooter',\n 'ProfileToggleTheme',\n ];\n\n let newProps: Partial<{\n store: DropdownStoreApi;\n children: ReactNode;\n }> = {};\n\n if (allowed.includes(displayName)) {\n newProps.store = store;\n }\n\n if (typedChild.props.children) {\n newProps.children = injectStore(typedChild.props.children, store);\n }\n\n return cloneElement(typedChild, newProps);\n }\n\n return child;\n });\n};\n\ninterface DropdownMenuProps {\n children: ReactNode;\n open?: boolean;\n onOpenChange?: (open: boolean) => void;\n}\n\nconst DropdownMenu = ({\n children,\n open: propOpen,\n onOpenChange,\n}: DropdownMenuProps) => {\n const storeRef = useRef<DropdownStoreApi | null>(null);\n storeRef.current ??= createDropdownStore();\n const store = storeRef.current;\n const { open, setOpen: storeSetOpen } = useStore(store, (s) => s);\n\n const setOpen = (newOpen: boolean) => {\n storeSetOpen(newOpen);\n };\n\n const menuRef = useRef<HTMLDivElement | null>(null);\n\n const handleArrowDownOrArrowUp = (event: globalThis.KeyboardEvent) => {\n const menuContent = menuRef.current?.querySelector('[role=\"menu\"]');\n if (menuContent) {\n event.preventDefault();\n\n const items = Array.from(\n menuContent.querySelectorAll(\n '[role=\"menuitem\"]:not([aria-disabled=\"true\"])'\n )\n ).filter((el): el is HTMLElement => el instanceof HTMLElement);\n\n if (items.length === 0) return;\n\n const focusedItem = document.activeElement as HTMLElement;\n const currentIndex = items.indexOf(focusedItem);\n\n let nextIndex;\n if (event.key === 'ArrowDown') {\n nextIndex = currentIndex === -1 ? 0 : (currentIndex + 1) % items.length;\n } else {\n // ArrowUp\n nextIndex =\n currentIndex === -1\n ? items.length - 1\n : (currentIndex - 1 + items.length) % items.length;\n }\n\n items[nextIndex]?.focus();\n }\n };\n\n const handleDownkey = (event: globalThis.KeyboardEvent) => {\n if (event.key === 'Escape') {\n setOpen(false);\n } else if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {\n handleArrowDownOrArrowUp(event);\n }\n };\n\n const handleClickOutside = (event: Event) => {\n const target = event.target as Node;\n\n if (menuRef.current?.contains(target)) {\n return;\n }\n\n if (\n target instanceof Element &&\n target.closest('[data-dropdown-content=\"true\"]')\n ) {\n return;\n }\n\n setOpen(false);\n };\n\n useEffect(() => {\n if (open) {\n document.addEventListener('pointerdown', handleClickOutside);\n document.addEventListener('keydown', handleDownkey);\n }\n\n return () => {\n document.removeEventListener('pointerdown', handleClickOutside);\n document.removeEventListener('keydown', handleDownkey);\n };\n }, [open]);\n\n useEffect(() => {\n onOpenChange?.(open);\n }, [open, onOpenChange]);\n\n useEffect(() => {\n if (propOpen !== undefined) {\n setOpen(propOpen);\n }\n }, [propOpen]);\n\n return (\n <div className=\"relative\" ref={menuRef}>\n {injectStore(children, store)}\n </div>\n );\n};\n\n// Componentes genéricos do DropdownMenu\nconst DropdownMenuTrigger = forwardRef<\n HTMLButtonElement,\n ButtonHTMLAttributes<HTMLButtonElement> & {\n disabled?: boolean;\n store?: DropdownStoreApi;\n }\n>(({ className, children, onClick, store: externalStore, ...props }, ref) => {\n const store = useDropdownStore(externalStore);\n\n const open = useStore(store, (s) => s.open);\n const toggleOpen = () => store.setState({ open: !open });\n\n return (\n <button\n ref={ref}\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation();\n toggleOpen();\n onClick?.(e);\n }}\n aria-expanded={open}\n className={cn(\n 'appearance-none bg-transparent border-none p-0',\n className\n )}\n {...props}\n >\n {children}\n </button>\n );\n});\nDropdownMenuTrigger.displayName = 'DropdownMenuTrigger';\n\nconst ITEM_SIZE_CLASSES = {\n small: 'text-sm',\n medium: 'text-md',\n} as const;\n\nconst SIDE_CLASSES = {\n top: 'bottom-full',\n right: 'top-full',\n bottom: 'top-full',\n left: 'top-full',\n};\n\nconst ALIGN_CLASSES = {\n start: 'left-0',\n center: 'left-1/2 -translate-x-1/2',\n end: 'right-0',\n};\n\nconst MENUCONTENT_VARIANT_CLASSES = {\n menu: 'p-1',\n profile: 'p-6',\n};\n\nconst MenuLabel = forwardRef<\n HTMLDivElement,\n HTMLAttributes<HTMLDivElement> & {\n inset?: boolean;\n store?: DropdownStoreApi;\n }\n>(({ className, inset, store: _store, ...props }, ref) => {\n return (\n <div\n ref={ref}\n className={cn('text-sm w-full', inset ? 'pl-8' : '', className)}\n {...props}\n />\n );\n});\nMenuLabel.displayName = 'MenuLabel';\n\nconst DropdownMenuContent = forwardRef<\n HTMLDivElement,\n HTMLAttributes<HTMLDivElement> & {\n align?: 'start' | 'center' | 'end';\n side?: 'top' | 'right' | 'bottom' | 'left';\n variant?: 'menu' | 'profile';\n sideOffset?: number;\n store?: DropdownStoreApi;\n portal?: boolean;\n triggerRef?: RefObject<HTMLElement | null>;\n }\n>(\n (\n {\n className,\n align = 'start',\n side = 'bottom',\n variant = 'menu',\n sideOffset = 4,\n children,\n store: externalStore,\n portal = false,\n triggerRef,\n ...props\n },\n ref\n ) => {\n const store = useDropdownStore(externalStore);\n const open = useStore(store, (s) => s.open);\n const [isVisible, setIsVisible] = useState(open);\n const [portalPosition, setPortalPosition] = useState({ top: 0, left: 0 });\n const contentRef = useRef<HTMLDivElement>(null);\n\n useEffect(() => {\n if (open) {\n setIsVisible(true);\n } else {\n const timer = setTimeout(() => setIsVisible(false), 200);\n return () => clearTimeout(timer);\n }\n }, [open]);\n\n useLayoutEffect(() => {\n if (portal && open && triggerRef?.current) {\n const rect = triggerRef.current.getBoundingClientRect();\n let top = rect.bottom + sideOffset;\n let left = rect.left;\n\n // Handle horizontal sides (left/right)\n if (side === 'left') {\n left = rect.left - sideOffset;\n top = rect.top;\n } else if (side === 'right') {\n left = rect.right + sideOffset;\n top = rect.top;\n } else {\n // Handle vertical sides (top/bottom)\n if (align === 'end') {\n left = rect.right;\n } else if (align === 'center') {\n left = rect.left + rect.width / 2;\n }\n\n if (side === 'top') {\n top = rect.top - sideOffset;\n }\n }\n\n setPortalPosition({ top, left });\n }\n }, [portal, open, triggerRef, align, side, sideOffset]);\n\n if (!isVisible) return null;\n\n const getPositionClasses = () => {\n if (portal) {\n return 'fixed';\n }\n const vertical = SIDE_CLASSES[side];\n const horizontal = ALIGN_CLASSES[align];\n\n return `absolute ${vertical} ${horizontal}`;\n };\n\n const getPortalAlignStyle = () => {\n if (!portal) return {};\n\n const baseStyle: CSSProperties = {\n top: portalPosition.top,\n };\n\n if (align === 'end') {\n baseStyle.right = window.innerWidth - portalPosition.left;\n } else if (align === 'center') {\n baseStyle.left = portalPosition.left;\n baseStyle.transform = 'translateX(-50%)';\n } else {\n baseStyle.left = portalPosition.left;\n }\n\n return baseStyle;\n };\n\n const variantClasses = MENUCONTENT_VARIANT_CLASSES[variant];\n\n const content = (\n <div\n ref={portal ? contentRef : ref}\n role=\"menu\"\n data-dropdown-content=\"true\"\n className={`\n bg-background z-50 min-w-[210px] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md border-border-100\n ${open ? 'animate-in fade-in-0 zoom-in-95' : 'animate-out fade-out-0 zoom-out-95'}\n ${getPositionClasses()}\n ${variantClasses}\n ${className}\n `}\n style={{\n ...(portal\n ? getPortalAlignStyle()\n : {\n marginTop: side === 'bottom' ? sideOffset : undefined,\n marginBottom: side === 'top' ? sideOffset : undefined,\n marginLeft: side === 'right' ? sideOffset : undefined,\n marginRight: side === 'left' ? sideOffset : undefined,\n }),\n }}\n {...props}\n >\n {children}\n </div>\n );\n\n if (portal && typeof document !== 'undefined') {\n return createPortal(content, document.body);\n }\n\n return content;\n }\n);\nDropdownMenuContent.displayName = 'DropdownMenuContent';\n\nconst DropdownMenuItem = forwardRef<\n HTMLDivElement,\n HTMLAttributes<HTMLDivElement> & {\n inset?: boolean;\n size?: 'small' | 'medium';\n iconLeft?: ReactNode;\n iconRight?: ReactNode;\n disabled?: boolean;\n variant?: 'profile' | 'menu';\n store?: DropdownStoreApi;\n preventClose?: boolean;\n }\n>(\n (\n {\n className,\n size = 'small',\n children,\n iconRight,\n iconLeft,\n disabled = false,\n onClick,\n variant = 'menu',\n store: externalStore,\n preventClose = false,\n ...props\n },\n ref\n ) => {\n const store = useDropdownStore(externalStore);\n const setOpen = useStore(store, (s) => s.setOpen);\n const sizeClasses = ITEM_SIZE_CLASSES[size];\n\n const handleClick = (\n e: MouseEvent<HTMLDivElement> | KeyboardEvent<HTMLDivElement>\n ) => {\n if (disabled) {\n e.preventDefault();\n e.stopPropagation();\n return;\n }\n if (e.type === 'click') {\n onClick?.(e as MouseEvent<HTMLDivElement>);\n } else if (e.type === 'keydown') {\n // For keyboard events, call onClick if Enter or Space was pressed\n if (\n (e as KeyboardEvent<HTMLDivElement>).key === 'Enter' ||\n (e as KeyboardEvent<HTMLDivElement>).key === ' '\n ) {\n onClick?.(e as unknown as MouseEvent<HTMLDivElement>);\n }\n // honor any user-provided key handler\n props.onKeyDown?.(e as KeyboardEvent<HTMLDivElement>);\n }\n if (!preventClose) {\n setOpen(false);\n }\n };\n\n const getVariantClasses = () => {\n if (variant === 'profile') {\n return 'relative flex flex-row justify-between select-none items-center gap-2 rounded-sm p-4 text-sm outline-none transition-colors [&>svg]:size-6 [&>svg]:shrink-0';\n }\n return 'relative flex select-none items-center gap-2 rounded-sm p-3 text-sm outline-none transition-colors [&>svg]:size-4 [&>svg]:shrink-0';\n };\n\n const getVariantProps = () => {\n return variant === 'profile' ? { 'data-variant': 'profile' } : {};\n };\n\n return (\n <div\n ref={ref}\n role=\"menuitem\"\n {...getVariantProps()}\n aria-disabled={disabled}\n className={`\n focus-visible:bg-background-50\n ${getVariantClasses()}\n ${sizeClasses}\n ${className}\n ${\n disabled\n ? 'cursor-not-allowed text-text-400'\n : 'cursor-pointer hover:bg-background-50 text-text-700 focus:bg-accent focus:text-accent-foreground hover:bg-accent hover:text-accent-foreground'\n }\n `}\n onClick={handleClick}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n e.stopPropagation();\n handleClick(e);\n }\n }}\n tabIndex={disabled ? -1 : 0}\n {...props}\n >\n {iconLeft}\n <div className=\"w-full\">{children}</div>\n {iconRight}\n </div>\n );\n }\n);\nDropdownMenuItem.displayName = 'DropdownMenuItem';\n\nconst DropdownMenuSeparator = forwardRef<\n HTMLDivElement,\n HTMLAttributes<HTMLDivElement> & { store?: DropdownStoreApi }\n>(({ className, store: _store, ...props }, ref) => (\n <div\n ref={ref}\n className={cn('my-1 h-px bg-border-200', className)}\n {...props}\n />\n));\nDropdownMenuSeparator.displayName = 'DropdownMenuSeparator';\n\n// Componentes específicos do ProfileMenu\nconst ProfileMenuTrigger = forwardRef<\n HTMLButtonElement,\n ButtonHTMLAttributes<HTMLButtonElement> & { store?: DropdownStoreApi }\n>(({ className, onClick, store: externalStore, ...props }, ref) => {\n const store = useDropdownStore(externalStore);\n const open = useStore(store, (s) => s.open);\n const toggleOpen = () => store.setState({ open: !open });\n\n return (\n <button\n ref={ref}\n className={cn(\n 'rounded-lg size-10 bg-primary-50 flex items-center justify-center cursor-pointer',\n className\n )}\n onClick={(e) => {\n e.stopPropagation();\n toggleOpen();\n onClick?.(e);\n }}\n aria-expanded={open}\n {...props}\n >\n <span className=\"size-6 rounded-full bg-primary-100 flex items-center justify-center\">\n <User className=\"text-primary-950\" size={18} />\n </span>\n </button>\n );\n});\nProfileMenuTrigger.displayName = 'ProfileMenuTrigger';\n\nconst ProfileMenuHeader = forwardRef<\n HTMLDivElement,\n HTMLAttributes<HTMLDivElement> & {\n name: string;\n email: string;\n photoUrl?: string | null;\n store?: DropdownStoreApi;\n }\n>(({ className, name, email, photoUrl, store: _store, ...props }, ref) => {\n return (\n <div\n ref={ref}\n data-component=\"ProfileMenuHeader\"\n className={cn(\n 'flex flex-row gap-4 items-center min-w-[280px]',\n className\n )}\n {...props}\n >\n <span className=\"w-16 h-16 bg-primary-100 rounded-full flex items-center justify-center overflow-hidden flex-shrink-0\">\n {photoUrl ? (\n <img\n src={photoUrl}\n alt=\"Foto de perfil\"\n className=\"w-full h-full object-cover\"\n />\n ) : (\n <User size={34} className=\"text-primary-800\" />\n )}\n </span>\n <div className=\"flex flex-col min-w-0\">\n <Text\n size=\"xl\"\n weight=\"bold\"\n color=\"text-text-950\"\n className=\"truncate\"\n >\n {name}\n </Text>\n <Text size=\"md\" color=\"text-text-600\" className=\"truncate\">\n {email}\n </Text>\n </div>\n </div>\n );\n});\nProfileMenuHeader.displayName = 'ProfileMenuHeader';\n\nconst ProfileMenuInfo = forwardRef<\n HTMLDivElement,\n HTMLAttributes<HTMLDivElement> & {\n schoolName: string;\n classYearName: string;\n schoolYearName: string;\n store?: DropdownStoreApi;\n }\n>(\n (\n {\n className,\n schoolName,\n classYearName,\n schoolYearName,\n store: _store,\n ...props\n },\n ref\n ) => {\n return (\n <div\n ref={ref}\n data-component=\"ProfileMenuInfo\"\n className={cn('flex flex-row gap-4 items-center', className)}\n {...props}\n >\n <span className=\"w-16 h-16\" />\n <div className=\"flex flex-col \">\n <Text size=\"md\" color=\"text-text-600\">\n {schoolName}\n </Text>\n\n <span className=\"flex flex-row items-center gap-2\">\n <Text size=\"md\" color=\"text-text-600\">\n {classYearName}\n </Text>\n <p className=\"text-text-600 text-xs align-middle\">●</p>\n <Text size=\"md\" color=\"text-text-600\">\n {schoolYearName}\n </Text>\n </span>\n </div>\n </div>\n );\n }\n);\nProfileMenuInfo.displayName = 'ProfileMenuInfo';\n\nconst ProfileToggleTheme = ({\n store: externalStore,\n ...props\n}: HTMLAttributes<HTMLDivElement> & { store?: DropdownStoreApi }) => {\n const { themeMode, setTheme } = useTheme();\n const [modalThemeToggle, setModalThemeToggle] = useState(false);\n const [selectedTheme, setSelectedTheme] = useState<ThemeMode>(themeMode);\n\n const internalStoreRef = useRef<DropdownStoreApi | null>(null);\n internalStoreRef.current ??= createDropdownStore();\n const store = externalStore ?? internalStoreRef.current;\n const setOpen = useStore(store, (s) => s.setOpen);\n\n const handleClick = (e: MouseEvent<HTMLDivElement>) => {\n e.preventDefault();\n e.stopPropagation();\n setModalThemeToggle(true);\n };\n\n const handleSave = () => {\n setTheme(selectedTheme);\n setModalThemeToggle(false);\n setOpen(false); // Close dropdown after saving\n };\n\n const handleCancel = () => {\n setSelectedTheme(themeMode); // Reset to current theme\n setModalThemeToggle(false);\n setOpen(false); // Close dropdown after canceling\n };\n\n return (\n <>\n <DropdownMenuItem\n variant=\"profile\"\n preventClose={true}\n store={store}\n iconLeft={\n <svg\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 25 25\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <path\n d=\"M12.5 2.75C15.085 2.75276 17.5637 3.78054 19.3916 5.6084C21.2195 7.43628 22.2473 9.915 22.25 12.5C22.25 14.4284 21.6778 16.3136 20.6064 17.917C19.5352 19.5201 18.0128 20.7699 16.2314 21.5078C14.4499 22.2458 12.489 22.4387 10.5977 22.0625C8.70642 21.6863 6.96899 20.758 5.60547 19.3945C4.24197 18.031 3.31374 16.2936 2.9375 14.4023C2.56129 12.511 2.75423 10.5501 3.49219 8.76855C4.23012 6.98718 5.47982 5.46483 7.08301 4.39355C8.68639 3.32221 10.5716 2.75 12.5 2.75ZM11.75 4.28516C9.70145 4.47452 7.7973 5.42115 6.41016 6.94043C5.02299 8.4599 4.25247 10.4426 4.25 12.5C4.25247 14.5574 5.02299 16.5401 6.41016 18.0596C7.7973 19.5789 9.70145 20.5255 11.75 20.7148V4.28516Z\"\n fill=\"currentColor\"\n />\n </svg>\n }\n iconRight={<CaretRight />}\n onClick={handleClick}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n e.stopPropagation();\n setModalThemeToggle(true);\n }\n }}\n {...props}\n >\n <Text size=\"md\" color=\"text-text-700\">\n Aparência\n </Text>\n </DropdownMenuItem>\n\n <Modal\n isOpen={modalThemeToggle}\n onClose={handleCancel}\n title=\"Aparência\"\n size=\"md\"\n footer={\n <div className=\"flex gap-3\">\n <Button variant=\"outline\" onClick={handleCancel}>\n Cancelar\n </Button>\n <Button variant=\"solid\" onClick={handleSave}>\n Salvar\n </Button>\n </div>\n }\n >\n <div className=\"flex flex-col\">\n <p className=\"text-sm text-text-500\">Escolha o tema:</p>\n <ThemeToggle variant=\"with-save\" onToggle={setSelectedTheme} />\n </div>\n </Modal>\n </>\n );\n};\nProfileToggleTheme.displayName = 'ProfileToggleTheme';\n\nconst ProfileMenuSection = forwardRef<\n HTMLDivElement,\n HTMLAttributes<HTMLDivElement> & { store?: DropdownStoreApi }\n>(({ className, children, store: _store, ...props }, ref) => {\n return (\n <div ref={ref} className={cn('flex flex-col p-2', className)} {...props}>\n {children}\n </div>\n );\n});\nProfileMenuSection.displayName = 'ProfileMenuSection';\n\nconst ProfileMenuFooter = ({\n className,\n disabled = false,\n onClick,\n store: externalStore,\n ...props\n}: HTMLAttributes<HTMLButtonElement> & {\n disabled?: boolean;\n store?: DropdownStoreApi;\n}) => {\n const store = useDropdownStore(externalStore);\n const setOpen = useStore(store, (s) => s.setOpen);\n\n return (\n <Button\n variant=\"outline\"\n className={cn('w-full', className)}\n disabled={disabled}\n onClick={(e) => {\n setOpen(false);\n onClick?.(e);\n }}\n {...props}\n >\n <span className=\"mr-2 flex items-center\">\n <SignOut className=\"text-inherit\" />\n </span>\n <Text color=\"inherit\">Sair</Text>\n </Button>\n );\n};\nProfileMenuFooter.displayName = 'ProfileMenuFooter';\n\n// Exportações\nexport default DropdownMenu;\nexport {\n // Componentes genéricos\n DropdownMenuTrigger,\n DropdownMenuContent,\n DropdownMenuItem,\n MenuLabel,\n DropdownMenuSeparator,\n\n // Componentes específicos do ProfileMenu\n ProfileMenuTrigger,\n ProfileMenuHeader,\n ProfileMenuSection,\n ProfileMenuFooter,\n ProfileToggleTheme,\n ProfileMenuInfo,\n};\n","import { clsx, type ClassValue } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n\nexport { syncDropdownState } from './dropdown';\nexport {\n getSelectedIdsFromCategories,\n toggleArrayItem,\n toggleSingleValue,\n areFiltersEqual,\n} from './activityFilters';\nexport {\n mapQuestionTypeToEnum,\n mapQuestionTypeToEnumRequired,\n} from './questionTypeUtils';\nexport {\n getStatusBadgeConfig,\n formatTimeSpent,\n formatQuestionNumbers,\n formatDateToBrazilian,\n} from './activityDetailsUtils';\n\n/**\n * Retorna a cor hexadecimal com opacidade 0.3 (4d) se não estiver em dark mode.\n * Se estiver em dark mode, retorna a cor original.\n *\n * @param hexColor - Cor hexadecimal (ex: \"#0066b8\" ou \"0066b8\")\n * @param isDark - booleano indicando se está em dark mode\n * @returns string - cor hexadecimal com opacidade se necessário\n */\nexport function getSubjectColorWithOpacity(\n hexColor: string | undefined,\n isDark: boolean\n): string | undefined {\n if (!hexColor) return undefined;\n // Remove o '#' se existir\n let color = hexColor.replace(/^#/, '').toLowerCase();\n\n if (isDark) {\n // Se está em dark mode, sempre remove opacidade se existir\n if (color.length === 8) {\n color = color.slice(0, 6);\n }\n return `#${color}`;\n } else {\n // Se não está em dark mode (light mode)\n let resultColor: string;\n if (color.length === 6) {\n // Adiciona opacidade 0.3 (4D) para cores de 6 dígitos\n resultColor = `#${color}4d`;\n } else if (color.length === 8) {\n // Já tem opacidade, retorna como está\n resultColor = `#${color}`;\n } else {\n // Para outros tamanhos (3, 4, 5 dígitos), retorna como está\n resultColor = `#${color}`;\n }\n return resultColor;\n }\n}\n","import { ButtonHTMLAttributes, ReactNode } from 'react';\nimport { cn } from '../../utils/utils';\n\n/**\n * Lookup table for variant and action class combinations\n */\nconst VARIANT_ACTION_CLASSES = {\n solid: {\n primary:\n 'bg-primary-950 text-text border border-primary-950 hover:bg-primary-800 hover:border-primary-800 focus-visible:outline-none focus-visible:bg-primary-950 focus-visible:ring-2 focus-visible:ring-offset-0 focus-visible:ring-indicator-info active:bg-primary-700 active:border-primary-700 disabled:bg-primary-500 disabled:border-primary-500 disabled:opacity-40 disabled:cursor-not-allowed',\n positive:\n 'bg-success-500 text-text border border-success-500 hover:bg-success-600 hover:border-success-600 focus-visible:outline-none focus-visible:bg-success-500 focus-visible:ring-2 focus-visible:ring-offset-0 focus-visible:ring-indicator-info active:bg-success-700 active:border-success-700 disabled:bg-success-500 disabled:border-success-500 disabled:opacity-40 disabled:cursor-not-allowed',\n negative:\n 'bg-error-500 text-text border border-error-500 hover:bg-error-600 hover:border-error-600 focus-visible:outline-none focus-visible:bg-error-500 focus-visible:ring-2 focus-visible:ring-offset-0 focus-visible:ring-indicator-info active:bg-error-700 active:border-error-700 disabled:bg-error-500 disabled:border-error-500 disabled:opacity-40 disabled:cursor-not-allowed',\n },\n outline: {\n primary:\n 'bg-transparent text-primary-950 border border-primary-950 hover:bg-background-50 hover:text-primary-400 hover:border-primary-400 focus-visible:border-0 focus-visible:outline-none focus-visible:text-primary-600 focus-visible:ring-2 focus-visible:ring-offset-0 focus-visible:ring-indicator-info active:text-primary-700 active:border-primary-700 disabled:opacity-40 disabled:cursor-not-allowed',\n positive:\n 'bg-transparent text-success-500 border border-success-300 hover:bg-background-50 hover:text-success-400 hover:border-success-400 focus-visible:border-0 focus-visible:outline-none focus-visible:text-success-600 focus-visible:ring-2 focus-visible:ring-offset-0 focus-visible:ring-indicator-info active:text-success-700 active:border-success-700 disabled:opacity-40 disabled:cursor-not-allowed',\n negative:\n 'bg-transparent text-error-500 border border-error-300 hover:bg-background-50 hover:text-error-400 hover:border-error-400 focus-visible:border-0 focus-visible:outline-none focus-visible:text-error-600 focus-visible:ring-2 focus-visible:ring-offset-0 focus-visible:ring-indicator-info active:text-error-700 active:border-error-700 disabled:opacity-40 disabled:cursor-not-allowed',\n },\n link: {\n primary:\n 'bg-transparent text-primary-950 hover:text-primary-400 focus-visible:outline-none focus-visible:text-primary-600 focus-visible:ring-2 focus-visible:ring-offset-0 focus-visible:ring-indicator-info active:text-primary-700 disabled:opacity-40 disabled:cursor-not-allowed',\n positive:\n 'bg-transparent text-success-500 hover:text-success-400 focus-visible:outline-none focus-visible:text-success-600 focus-visible:ring-2 focus-visible:ring-offset-0 focus-visible:ring-indicator-info active:text-success-700 disabled:opacity-40 disabled:cursor-not-allowed',\n negative:\n 'bg-transparent text-error-500 hover:text-error-400 focus-visible:outline-none focus-visible:text-error-600 focus-visible:ring-2 focus-visible:ring-offset-0 focus-visible:ring-indicator-info active:text-error-700 disabled:opacity-40 disabled:cursor-not-allowed',\n },\n} as const;\n\n/**\n * Lookup table for size classes\n */\nconst SIZE_CLASSES = {\n 'extra-small': 'text-xs px-3.5 py-2',\n small: 'text-sm px-4 py-2.5',\n medium: 'text-md px-5 py-2.5',\n large: 'text-lg px-6 py-3',\n 'extra-large': 'text-lg px-7 py-3.5',\n} as const;\n\n/**\n * Button component props interface\n */\ntype ButtonProps = {\n /** Content to be displayed inside the button */\n children: ReactNode;\n /** Ícone à esquerda do texto */\n iconLeft?: ReactNode;\n /** Ícone à direita do texto */\n iconRight?: ReactNode;\n /** Size of the button */\n size?: 'extra-small' | 'small' | 'medium' | 'large' | 'extra-large';\n /** Visual variant of the button */\n variant?: 'solid' | 'outline' | 'link';\n /** Action type of the button */\n action?: 'primary' | 'positive' | 'negative';\n /** Additional CSS classes to apply */\n className?: string;\n} & ButtonHTMLAttributes<HTMLButtonElement>;\n\n/**\n * Button component for Analytica Ensino platforms\n *\n * A flexible button component with multiple variants, sizes and actions.\n *\n * @param children - The content to display inside the button\n * @param size - The size variant (extra-small, small, medium, large, extra-large)\n * @param variant - The visual style variant (solid, outline, link)\n * @param action - The action type (primary, positive, negative)\n * @param className - Additional CSS classes\n * @param props - All other standard button HTML attributes\n * @returns A styled button element\n *\n * @example\n * ```tsx\n * <Button variant=\"solid\" action=\"primary\" size=\"medium\" onClick={() => console.log('clicked')}>\n * Click me\n * </Button>\n * ```\n */\nconst Button = ({\n children,\n iconLeft,\n iconRight,\n size = 'medium',\n variant = 'solid',\n action = 'primary',\n className = '',\n disabled,\n type = 'button',\n ...props\n}: ButtonProps) => {\n // Get classes from lookup tables\n const sizeClasses = SIZE_CLASSES[size];\n const variantClasses = VARIANT_ACTION_CLASSES[variant][action];\n\n const baseClasses =\n 'inline-flex items-center justify-center rounded-full cursor-pointer font-medium';\n\n return (\n <button\n className={cn(baseClasses, variantClasses, sizeClasses, className)}\n disabled={disabled}\n type={type}\n {...props}\n >\n {iconLeft && <span className=\"mr-2 flex items-center\">{iconLeft}</span>}\n {children}\n {iconRight && <span className=\"ml-2 flex items-center\">{iconRight}</span>}\n </button>\n );\n};\n\nexport default Button;\n","import { ComponentPropsWithoutRef, ElementType, ReactNode } from 'react';\nimport { cn } from '../../utils/utils';\n\n/**\n * Base text component props\n */\ntype BaseTextProps = {\n /** Content to be displayed */\n children?: ReactNode;\n /** Text size variant */\n size?:\n | '2xs'\n | 'xs'\n | 'sm'\n | 'md'\n | 'lg'\n | 'xl'\n | '2xl'\n | '3xl'\n | '4xl'\n | '5xl'\n | '6xl';\n /** Font weight variant */\n weight?:\n | 'hairline'\n | 'light'\n | 'normal'\n | 'medium'\n | 'semibold'\n | 'bold'\n | 'extrabold'\n | 'black';\n /** Color variant - white for light backgrounds, black for dark backgrounds */\n color?: string;\n /** Additional CSS classes to apply */\n className?: string;\n};\n\n/**\n * Polymorphic text component props that ensures type safety based on the 'as' prop\n */\ntype TextProps<T extends ElementType = 'p'> = BaseTextProps & {\n /** HTML tag to render */\n as?: T;\n} & Omit<ComponentPropsWithoutRef<T>, keyof BaseTextProps>;\n\n/**\n * Text component for Analytica Ensino platforms\n *\n * A flexible polymorphic text component with multiple sizes, weights, and colors.\n * Automatically adapts to dark and light themes with full type safety.\n *\n * @param children - The content to display\n * @param size - The text size variant (2xs, xs, sm, md, lg, xl, 2xl, 3xl, 4xl, 5xl, 6xl)\n * @param weight - The font weight variant (hairline, light, normal, medium, semibold, bold, extrabold, black)\n * @param color - The color variant - adapts to theme\n * @param as - The HTML tag to render - determines allowed attributes via TypeScript\n * @param className - Additional CSS classes\n * @param props - HTML attributes valid for the chosen tag only\n * @returns A styled text element with type-safe attributes\n *\n * @example\n * ```tsx\n * <Text size=\"lg\" weight=\"bold\" color=\"text-info-800\">\n * This is a large, bold text\n * </Text>\n *\n * <Text as=\"a\" href=\"/link\" target=\"_blank\">\n * Link with type-safe anchor attributes\n * </Text>\n *\n * <Text as=\"button\" onClick={handleClick} disabled>\n * Button with type-safe button attributes\n * </Text>\n * ```\n */\nconst Text = <T extends ElementType = 'p'>({\n children,\n size = 'md',\n weight = 'normal',\n color = 'text-text-950',\n as,\n className = '',\n ...props\n}: TextProps<T>) => {\n let sizeClasses = '';\n let weightClasses = '';\n\n // Text size classes mapping\n const sizeClassMap = {\n '2xs': 'text-2xs',\n xs: 'text-xs',\n sm: 'text-sm',\n md: 'text-md',\n lg: 'text-lg',\n xl: 'text-xl',\n '2xl': 'text-2xl',\n '3xl': 'text-3xl',\n '4xl': 'text-4xl',\n '5xl': 'text-5xl',\n '6xl': 'text-6xl',\n } as const;\n\n sizeClasses = sizeClassMap[size] ?? sizeClassMap.md;\n\n // Font weight classes mapping\n const weightClassMap = {\n hairline: 'font-hairline',\n light: 'font-light',\n normal: 'font-normal',\n medium: 'font-medium',\n semibold: 'font-semibold',\n bold: 'font-bold',\n extrabold: 'font-extrabold',\n black: 'font-black',\n } as const;\n\n weightClasses = weightClassMap[weight] ?? weightClassMap.normal;\n\n const baseClasses = 'font-primary';\n const Component = as ?? ('p' as ElementType);\n\n return (\n <Component\n className={cn(baseClasses, sizeClasses, weightClasses, color, className)}\n {...props}\n >\n {children}\n </Component>\n );\n};\n\nexport default Text;\n","import { ReactNode, useEffect, useId } from 'react';\nimport { X } from 'phosphor-react';\nimport { cn } from '../../utils/utils';\nimport Button from '../Button/Button';\nimport {\n isYouTubeUrl,\n getYouTubeVideoId,\n getYouTubeEmbedUrl,\n} from './utils/videoUtils';\n\n/**\n * Lookup table for size classes\n */\nconst SIZE_CLASSES = {\n xs: 'max-w-[360px]',\n sm: 'max-w-[420px]',\n md: 'max-w-[510px]',\n lg: 'max-w-[640px]',\n xl: 'max-w-[970px]',\n} as const;\n\n/**\n * Modal component props interface\n */\ntype ModalProps = {\n contentClassName?: string;\n /** Whether the modal is open */\n isOpen: boolean;\n /** Function to close the modal */\n onClose: () => void;\n /** Modal title */\n title: string;\n /** Modal description/content */\n children?: ReactNode;\n /** Size of the modal */\n size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';\n /** Additional CSS classes for the modal content */\n className?: string;\n /** Whether pressing Escape should close the modal */\n closeOnEscape?: boolean;\n /** Footer content (typically buttons) */\n footer?: ReactNode;\n /** Hide the close button */\n hideCloseButton?: boolean;\n /** Modal variant */\n variant?: 'default' | 'activity';\n /** Description for activity variant */\n description?: string;\n /** Image URL for activity variant */\n image?: string;\n /** Alt text for activity image (leave empty for decorative images) */\n imageAlt?: string;\n /** Action link for activity variant */\n actionLink?: string;\n /** Action button label for activity variant */\n actionLabel