diff --git a/Screenshot from 2025-12-05 15-02-44.png b/Screenshot from 2025-12-05 15-02-44.png new file mode 100644 index 0000000..dfb25e1 Binary files /dev/null and b/Screenshot from 2025-12-05 15-02-44.png differ diff --git a/web/maplefile-frontend/src/components/UIX/IconPicker/IconPicker.jsx b/web/maplefile-frontend/src/components/UIX/IconPicker/IconPicker.jsx index 094a4dc..7e7f62f 100644 --- a/web/maplefile-frontend/src/components/UIX/IconPicker/IconPicker.jsx +++ b/web/maplefile-frontend/src/components/UIX/IconPicker/IconPicker.jsx @@ -1,6 +1,7 @@ // File: src/components/UIX/IconPicker/IconPicker.jsx -// Icon picker component for selecting emojis or predefined icons for collections -import React, { useState } from "react"; +// UIX version - Icon picker component for selecting emojis or predefined icons +// Theme-aware with proper UIX component usage +import React, { useState, useCallback, useMemo } from "react"; import { XMarkIcon, FolderIcon, @@ -41,6 +42,9 @@ import { TrophyIcon, UserGroupIcon, } from "@heroicons/react/24/outline"; +import { useUIXTheme } from "../themes/useUIXTheme"; +import Button from "../Button/Button"; +import Tabs from "../Tabs/Tabs"; // Predefined icons with their identifiers and labels const PREDEFINED_ICONS = [ @@ -84,103 +88,58 @@ const PREDEFINED_ICONS = [ // Comprehensive emoji collection organized by logical categories const EMOJI_CATEGORIES = { - // Most commonly used for file organization - "Files & Folders": [ + "Folders & Work": [ "๐Ÿ“", "๐Ÿ“‚", "๐Ÿ—‚๏ธ", "๐Ÿ“‘", "๐Ÿ“„", "๐Ÿ“ƒ", "๐Ÿ“‹", "๐Ÿ“", "โœ๏ธ", "๐Ÿ–Š๏ธ", "๐Ÿ“Ž", "๐Ÿ“Œ", "๐Ÿ”–", "๐Ÿท๏ธ", "๐Ÿ“ฐ", "๐Ÿ—ƒ๏ธ", "๐Ÿ—„๏ธ", "๐Ÿ“ฆ", "๐Ÿ“ฅ", "๐Ÿ“ค", - ], - - // Work, business, and professional - "Work & Business": [ "๐Ÿ’ผ", "๐Ÿข", "๐Ÿ›๏ธ", "๐Ÿฆ", "๐Ÿ’ฐ", "๐Ÿ’ต", "๐Ÿ’ณ", "๐Ÿงพ", "๐Ÿ“Š", "๐Ÿ“ˆ", "๐Ÿ“‰", "๐Ÿ’น", "๐Ÿ—“๏ธ", "๐Ÿ“…", "โฐ", "โŒš", "๐Ÿ–ฅ๏ธ", "๐Ÿ’ป", "โŒจ๏ธ", "๐Ÿ–จ๏ธ", ], - - // Technology, electronics, and connectivity - "Tech & Devices": [ + "Tech & Media": [ "๐Ÿ“ฑ", "๐Ÿ“ฒ", "โ˜Ž๏ธ", "๐Ÿ“ž", "๐Ÿ“Ÿ", "๐Ÿ“ ", "๐Ÿ”Œ", "๐Ÿ”‹", "๐Ÿ’พ", "๐Ÿ’ฟ", "๐Ÿ“€", "๐Ÿ–ฑ๏ธ", "๐Ÿ–ฒ๏ธ", "๐ŸŽฎ", "๐Ÿ•น๏ธ", "๐Ÿ›œ", "๐Ÿ“ก", "๐Ÿ“บ", "๐Ÿ“ป", "๐ŸŽ™๏ธ", - ], - - // Media, entertainment, and creativity - "Media & Creative": [ "๐Ÿ“ท", "๐Ÿ“ธ", "๐Ÿ“น", "๐ŸŽฅ", "๐ŸŽฌ", "๐ŸŽž๏ธ", "๐Ÿ“ฝ๏ธ", "๐ŸŽต", "๐ŸŽถ", "๐ŸŽค", "๐ŸŽง", "๐ŸŽผ", "๐ŸŽน", "๐ŸŽธ", "๐Ÿฅ", "๐ŸŽจ", "๐Ÿ–ผ๏ธ", "๐ŸŽญ", "๐ŸŽช", "๐ŸŽ ", + "๐Ÿ”’", "๐Ÿ”“", "๐Ÿ”", "๐Ÿ”‘", "๐Ÿ—๏ธ", "๐Ÿ›ก๏ธ", "โš”๏ธ", "๐Ÿ”ซ", "๐Ÿšจ", "๐Ÿš”", + "๐Ÿ‘ฎ", "๐Ÿ•ต๏ธ", "๐Ÿฆบ", "๐Ÿงฏ", "๐Ÿช–", "โ›‘๏ธ", "๐Ÿ”", "๐Ÿ‘๏ธโ€๐Ÿ—จ๏ธ", "๐Ÿ›‚", + "๐Ÿ’ฌ", "๐Ÿ’ญ", "๐Ÿ—จ๏ธ", "๐Ÿ—ฏ๏ธ", "๐Ÿ“ง", "๐Ÿ“จ", "๐Ÿ“ฉ", "๐Ÿ“ฎ", "๐Ÿ“ช", "๐Ÿ“ซ", + "๐Ÿ“ฌ", "๐Ÿ“ญ", "โœ‰๏ธ", "๐Ÿ’Œ", "๐Ÿ“ฏ", "๐Ÿ””", "๐Ÿ”•", "๐Ÿ“ข", "๐Ÿ“ฃ", "๐Ÿ—ฃ๏ธ", ], - - // Education, learning, and research "Education & Science": [ "๐Ÿ“š", "๐Ÿ“–", "๐Ÿ“•", "๐Ÿ“—", "๐Ÿ“˜", "๐Ÿ“™", "๐ŸŽ“", "๐Ÿซ", "โœ๏ธ", "๐Ÿ“", "๐Ÿ“", "๐Ÿ”ฌ", "๐Ÿ”ญ", "๐Ÿงช", "๐Ÿงซ", "๐Ÿงฌ", "๐Ÿ”", "๐Ÿ”Ž", "๐Ÿ’ก", "๐Ÿ“ก", ], - - // Communication and social - "Communication": [ - "๐Ÿ’ฌ", "๐Ÿ’ญ", "๐Ÿ—จ๏ธ", "๐Ÿ—ฏ๏ธ", "๐Ÿ“ง", "๐Ÿ“จ", "๐Ÿ“ฉ", "๐Ÿ“ฎ", "๐Ÿ“ช", "๐Ÿ“ซ", - "๐Ÿ“ฌ", "๐Ÿ“ญ", "โœ‰๏ธ", "๐Ÿ’Œ", "๐Ÿ“ฏ", "๐Ÿ””", "๐Ÿ”•", "๐Ÿ“ข", "๐Ÿ“ฃ", "๐Ÿ—ฃ๏ธ", - ], - - // Home, family, and personal life "Home & Life": [ "๐Ÿ ", "๐Ÿก", "๐Ÿ˜๏ธ", "๐Ÿ›๏ธ", "๐Ÿ›‹๏ธ", "๐Ÿช‘", "๐Ÿšฟ", "๐Ÿ›", "๐Ÿงน", "๐Ÿงบ", "๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ", "๐Ÿ‘ช", "โค๏ธ", "๐Ÿ’•", "๐Ÿ’", "๐Ÿ’–", "๐Ÿงธ", "๐ŸŽ", "๐ŸŽ€", "๐ŸŽˆ", ], - - // Health, fitness, and wellness - "Health & Wellness": [ + "Health & Food": [ "๐Ÿฅ", "๐Ÿ’Š", "๐Ÿ’‰", "๐Ÿฉบ", "๐Ÿฉน", "๐Ÿฉผ", "โ™ฟ", "๐Ÿง˜", "๐Ÿƒ", "๐Ÿšด", "๐Ÿ‹๏ธ", "๐Ÿคธ", "โš•๏ธ", "๐Ÿฉธ", "๐Ÿง ", "๐Ÿ‘๏ธ", "๐Ÿฆท", "๐Ÿ’ช", "๐Ÿงฌ", "๐ŸŽ", - ], - - // Food and beverages - "Food & Drinks": [ "๐Ÿ•", "๐Ÿ”", "๐ŸŸ", "๐ŸŒฎ", "๐ŸŒฏ", "๐Ÿœ", "๐Ÿ", "๐Ÿฃ", "๐Ÿฑ", "๐Ÿฅ—", "๐Ÿฐ", "๐ŸŽ‚", "๐Ÿง", "๐Ÿฉ", "๐Ÿช", "โ˜•", "๐Ÿต", "๐Ÿฅค", "๐Ÿท", "๐Ÿบ", ], - - // Travel, transportation, and places "Travel & Places": [ "โœˆ๏ธ", "๐Ÿš€", "๐Ÿ›ธ", "๐Ÿš", "๐Ÿš‚", "๐Ÿš—", "๐Ÿš•", "๐ŸšŒ", "๐Ÿšข", "โ›ต", "๐Ÿ—บ๏ธ", "๐Ÿงญ", "๐Ÿ–๏ธ", "๐Ÿ”๏ธ", "๐Ÿ•๏ธ", "๐Ÿ—ฝ", "๐Ÿ—ผ", "๐Ÿฐ", "โ›บ", "๐ŸŒ", ], - - // Sports, games, and hobbies "Sports & Hobbies": [ "โšฝ", "๐Ÿ€", "๐Ÿˆ", "โšพ", "๐ŸŽพ", "๐Ÿ", "๐Ÿ“", "๐Ÿธ", "๐ŸŽฏ", "๐ŸŽฑ", "๐ŸŽณ", "๐Ÿ†", "๐Ÿฅ‡", "๐Ÿฅˆ", "๐Ÿฅ‰", "๐ŸŽฒ", "โ™Ÿ๏ธ", "๐Ÿงฉ", "๐ŸŽฐ", "๐ŸŽฎ", ], - - // Nature, weather, and environment - "Nature & Weather": [ + "Nature & Animals": [ "๐ŸŒธ", "๐ŸŒบ", "๐ŸŒป", "๐ŸŒน", "๐ŸŒท", "๐ŸŒด", "๐ŸŒฒ", "๐Ÿ€", "๐ŸŒฟ", "๐Ÿƒ", "โ˜€๏ธ", "๐ŸŒ™", "โญ", "๐ŸŒˆ", "โ˜๏ธ", "๐ŸŒง๏ธ", "โ„๏ธ", "๐Ÿ”ฅ", "๐Ÿ’ง", "๐ŸŒŠ", - ], - - // Animals and pets - "Animals": [ "๐Ÿถ", "๐Ÿฑ", "๐Ÿญ", "๐Ÿน", "๐Ÿฐ", "๐ŸฆŠ", "๐Ÿป", "๐Ÿผ", "๐Ÿจ", "๐Ÿฆ", "๐Ÿฏ", "๐Ÿฎ", "๐Ÿท", "๐Ÿธ", "๐Ÿต", "๐Ÿ”", "๐Ÿง", "๐Ÿฆ", "๐Ÿฆ‹", "๐Ÿ", ], - - // Symbols, signs, and indicators "Symbols & Status": [ "โœ…", "โŒ", "โญ•", "โ—", "โ“", "๐Ÿ’ฏ", "๐Ÿ”ด", "๐ŸŸ ", "๐ŸŸก", "๐ŸŸข", "๐Ÿ”ต", "๐ŸŸฃ", "โšซ", "โšช", "๐Ÿ”ถ", "๐Ÿ”ท", "๐Ÿ’ ", "๐Ÿ”˜", "๐Ÿ", "๐Ÿšฉ", ], - - // Security and protection - "Security": [ - "๐Ÿ”’", "๐Ÿ”“", "๐Ÿ”", "๐Ÿ”‘", "๐Ÿ—๏ธ", "๐Ÿ›ก๏ธ", "โš”๏ธ", "๐Ÿ”ซ", "๐Ÿšจ", "๐Ÿš”", - "๐Ÿ‘ฎ", "๐Ÿ•ต๏ธ", "๐Ÿฆบ", "๐Ÿงฏ", "๐Ÿช–", "โ›‘๏ธ", "๐Ÿ”", "๐Ÿ”’", "๐Ÿ‘๏ธโ€๐Ÿ—จ๏ธ", "๐Ÿ›‚", - ], - - // Time and scheduling "Time & Planning": [ "โฐ", "โฑ๏ธ", "โฒ๏ธ", "๐Ÿ•", "๐Ÿ•‘", "๐Ÿ•’", "๐Ÿ•“", "๐Ÿ•”", "๐Ÿ••", "๐Ÿ•–", "๐Ÿ“…", "๐Ÿ“†", "๐Ÿ—“๏ธ", "โŒ›", "โณ", "๐Ÿ”œ", "๐Ÿ”™", "๐Ÿ”š", "๐Ÿ”›", "๐Ÿ”", ], - - // Celebration and events "Celebration": [ "๐ŸŽ‰", "๐ŸŽŠ", "๐ŸŽ‚", "๐ŸŽ", "๐ŸŽ€", "๐ŸŽˆ", "๐ŸŽ„", "๐ŸŽƒ", "๐ŸŽ†", "๐ŸŽ‡", "โœจ", "๐Ÿ’ซ", "๐ŸŒŸ", "โญ", "๐Ÿ…", "๐ŸŽ–๏ธ", "๐Ÿ†", "๐Ÿฅณ", "๐ŸŽฏ", "๐ŸŽช", @@ -188,146 +147,181 @@ const EMOJI_CATEGORIES = { }; const IconPicker = ({ value, onChange, onClose, isOpen }) => { + const { getThemeClasses } = useUIXTheme(); const [activeTab, setActiveTab] = useState("emoji"); - const [activeEmojiCategory, setActiveEmojiCategory] = useState("Files & Folders"); + const [activeEmojiCategory, setActiveEmojiCategory] = useState("Folders & Work"); + + // Memoize category keys + const categoryKeys = useMemo(() => Object.keys(EMOJI_CATEGORIES), []); + + // Memoize tabs configuration + const tabsConfig = useMemo(() => [ + { id: "emoji", label: "Emoji" }, + { id: "icons", label: "Icons" }, + ], []); + + // Handlers + const handleSelect = useCallback((iconValue) => { + onChange(iconValue); + onClose(); + }, [onChange, onClose]); + + const handleReset = useCallback(() => { + onChange(""); + onClose(); + }, [onChange, onClose]); + + const handleTabChange = useCallback((tab) => { + setActiveTab(tab); + }, []); + + const handleCategoryChange = useCallback((category) => { + setActiveEmojiCategory(category); + }, []); + + // Handle backdrop click + const handleBackdropClick = useCallback((e) => { + if (e.target === e.currentTarget) { + onClose(); + } + }, [onClose]); + + // Handle escape key + const handleKeyDown = useCallback((e) => { + if (e.key === "Escape") { + onClose(); + } + }, [onClose]); if (!isOpen) return null; - const handleSelect = (iconValue) => { - onChange(iconValue); - onClose(); - }; - - const handleReset = () => { - onChange(""); - onClose(); - }; - - // Check if current value is an icon or emoji - const isIconSelected = value?.startsWith("icon:"); - const selectedIconId = isIconSelected ? value.replace("icon:", "") : null; - - // Get category keys for rendering - const categoryKeys = Object.keys(EMOJI_CATEGORIES); + // Check if current value is an icon or emoji (with type safety) + const safeValue = typeof value === 'string' ? value : ''; + const isIconSelected = safeValue.startsWith("icon:"); + const selectedIconId = isIconSelected ? safeValue.replace("icon:", "") : null; return ( -
-
+
+
{/* Header */} -
-

Choose Icon

- +
{/* Tabs */} -
- - -
+ {/* Content */}
{activeTab === "emoji" ? (
{/* Category Sidebar */} -
+
{categoryKeys.map((category) => ( - + ))}
{/* Emoji Grid */}
-

+

{activeEmojiCategory}

-
+
{EMOJI_CATEGORIES[activeEmojiCategory].map((emoji, index) => ( - + ))}
) : ( -
+
{PREDEFINED_ICONS.map(({ id, Icon, label }) => ( - + ))}
)}
{/* Footer */} -
- - +
diff --git a/web/maplefile-frontend/src/components/UIX/Tabs/Tabs.jsx b/web/maplefile-frontend/src/components/UIX/Tabs/Tabs.jsx index 194c39d..32436ca 100644 --- a/web/maplefile-frontend/src/components/UIX/Tabs/Tabs.jsx +++ b/web/maplefile-frontend/src/components/UIX/Tabs/Tabs.jsx @@ -13,11 +13,14 @@ import { useUIXTheme } from "../themes/useUIXTheme.jsx"; * @param {function} onTabChange - Tab change handler for callback mode * @param {string} className - Additional CSS classes * @param {string} mode - 'callback' for onClick behavior, 'routing' for href navigation (default: 'callback') + * @param {string} variant - 'default' for standard tabs, 'pills' for pill-style, 'underline' for simple underline (default: 'default') + * @param {string} size - 'sm', 'md', 'lg' (default: 'md') + * @param {boolean} fullWidth - Whether tabs should take full width (default: false) */ // Tab Item Component - Separated for performance const TabItem = memo( - function TabItem({ tab, index, isActive, mode, onTabChange, themeClasses }) { + function TabItem({ tab, index, isActive, mode, onTabChange, themeClasses, variant, size, fullWidth }) { // Memoize click handler const handleClick = useCallback(() => { // Use tab's own onClick if provided, otherwise use parent onTabChange @@ -28,53 +31,88 @@ const TabItem = memo( } }, [tab, onTabChange]); + // Size classes + const sizeClasses = { + sm: "py-2 px-3 text-sm", + md: "py-3 px-4 text-sm sm:text-base", + lg: "py-3 sm:py-4 px-1 text-base sm:text-lg", + }; + // Memoize icon element const IconElement = useMemo(() => { if (!tab.icon) return null; - return ; - }, [tab.icon]); + const iconSize = size === "sm" ? "w-4 h-4" : size === "lg" ? "w-5 h-5" : "w-4 sm:w-5 h-4 sm:h-5"; + return ; + }, [tab.icon, size]); - // Memoize classes based on active state + // Memoize classes based on active state and variant const tabClasses = useMemo(() => { const baseClasses = [ - "border-b-4", - "border-transparent", - "py-3", - "sm:py-4", - "px-1", - "text-base", - "sm:text-lg", "font-medium", "whitespace-nowrap", "inline-flex", "items-center", + "justify-center", "transition-colors", "duration-200", + sizeClasses[size] || sizeClasses.md, ]; - // Determine hover border color based on theme - const hoverBorderClass = { - red: "hover:border-red-500", - blue: "hover:border-blue-500", - purple: "hover:border-purple-500", - green: "hover:border-green-500", - charcoal: "hover:border-slate-500", - }[themeClasses.currentTheme] || "hover:border-gray-500"; + if (fullWidth) { + baseClasses.push("flex-1"); + } - if (isActive) { - // Active tab: just show primary text, no border - baseClasses.push(themeClasses.textPrimary); + // Variant-specific styling + if (variant === "underline") { + // Simple underline style - good for modals + baseClasses.push("border-b-2"); + if (isActive) { + baseClasses.push(themeClasses.textAccent, themeClasses.borderAccent); + } else { + baseClasses.push( + "border-transparent", + themeClasses.textSecondary, + themeClasses.hoverTextPrimary + ); + } + } else if (variant === "pills") { + // Pill-style tabs + baseClasses.push("rounded-lg"); + if (isActive) { + baseClasses.push(themeClasses.bgAccentLight, themeClasses.textAccent); + } else { + baseClasses.push( + themeClasses.textSecondary, + themeClasses.hoverBgMuted, + themeClasses.hoverTextPrimary + ); + } } else { - // Inactive tab: secondary text with hover effect - baseClasses.push( - themeClasses.textSecondary, - themeClasses.hoverTextPrimary, - hoverBorderClass - ); + // Default variant - original behavior + baseClasses.push("border-b-4", "border-transparent"); + + // Determine hover border color based on theme + const hoverBorderClass = { + red: "hover:border-red-500", + blue: "hover:border-blue-500", + purple: "hover:border-purple-500", + green: "hover:border-green-500", + charcoal: "hover:border-slate-500", + }[themeClasses.currentTheme] || "hover:border-gray-500"; + + if (isActive) { + baseClasses.push(themeClasses.textPrimary); + } else { + baseClasses.push( + themeClasses.textSecondary, + themeClasses.hoverTextPrimary, + hoverBorderClass + ); + } } return baseClasses.filter(Boolean).join(" "); - }, [isActive, themeClasses]); + }, [isActive, themeClasses, variant, size, fullWidth]); // Routing mode with active state (non-interactive) if (mode === "routing" && isActive) { @@ -130,7 +168,10 @@ const TabItem = memo( prevProps.isActive === nextProps.isActive && prevProps.mode === nextProps.mode && prevProps.onTabChange === nextProps.onTabChange && - prevProps.themeClasses === nextProps.themeClasses + prevProps.themeClasses === nextProps.themeClasses && + prevProps.variant === nextProps.variant && + prevProps.size === nextProps.size && + prevProps.fullWidth === nextProps.fullWidth ); }, ); @@ -145,6 +186,9 @@ const Tabs = memo( onTabChange, className = "", mode = "callback", + variant = "default", + size = "md", + fullWidth = false, }) { const { getThemeClasses, currentTheme } = useUIXTheme(); @@ -153,7 +197,12 @@ const Tabs = memo( () => ({ textPrimary: getThemeClasses("text-primary"), textSecondary: getThemeClasses("text-secondary"), + textAccent: getThemeClasses("text-accent"), hoverTextPrimary: getThemeClasses("hover:text-primary"), + bgAccentLight: getThemeClasses("bg-accent-light"), + hoverBgMuted: getThemeClasses("hover:bg-muted"), + borderAccent: getThemeClasses("border-accent"), + borderSecondary: getThemeClasses("border-secondary"), currentTheme: currentTheme, }), [getThemeClasses, currentTheme], @@ -168,11 +217,28 @@ const Tabs = memo( return classes.join(" "); }, [className]); - // Memoize nav classes - add border bottom for separator - const navClasses = useMemo( - () => `px-4 sm:px-6 border-b border-gray-200 dark:border-gray-700`, - [], - ); + // Memoize nav classes based on variant + const navClasses = useMemo(() => { + if (variant === "underline") { + return `border-b ${themeClasses.borderSecondary}`; + } else if (variant === "pills") { + return ""; + } + return `px-4 sm:px-6 border-b border-gray-200 dark:border-gray-700`; + }, [variant, themeClasses.borderSecondary]); + + // Memoize inner nav classes + const innerNavClasses = useMemo(() => { + const classes = ["flex", "overflow-x-auto", "scrollbar-hide"]; + if (variant === "underline") { + // No margin adjustment needed for underline + } else if (variant === "pills") { + classes.push("space-x-2", "p-1"); + } else { + classes.push("-mb-px", "space-x-4", "sm:space-x-8"); + } + return classes.join(" "); + }, [variant]); // Memoize tab items const TabItems = useMemo(() => { @@ -189,16 +255,19 @@ const Tabs = memo( mode={mode} onTabChange={onTabChange} themeClasses={themeClasses} + variant={variant} + size={size} + fullWidth={fullWidth} /> ); }); - }, [tabs, mode, activeTab, onTabChange, themeClasses]); + }, [tabs, mode, activeTab, onTabChange, themeClasses, variant, size, fullWidth]); return (