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 */}
-
- setActiveTab("emoji")}
- className={`flex-1 py-3 text-sm font-medium transition-colors ${
- activeTab === "emoji"
- ? "text-red-800 border-b-2 border-red-800"
- : "text-gray-500 hover:text-gray-700"
- }`}
- >
- Emoji
-
- setActiveTab("icons")}
- className={`flex-1 py-3 text-sm font-medium transition-colors ${
- activeTab === "icons"
- ? "text-red-800 border-b-2 border-red-800"
- : "text-gray-500 hover:text-gray-700"
- }`}
- >
- Icons
-
-
+
{/* Content */}
{activeTab === "emoji" ? (
{/* Category Sidebar */}
-
+
{categoryKeys.map((category) => (
- setActiveEmojiCategory(category)}
- className={`w-full text-left px-3 py-2 text-xs font-medium rounded-lg transition-colors ${
+ onClick={() => handleCategoryChange(category)}
+ variant={activeEmojiCategory === category ? "ghost" : "ghost"}
+ size="sm"
+ className={`w-full justify-start text-xs ${
activeEmojiCategory === category
- ? "bg-red-100 text-red-800"
- : "text-gray-600 hover:bg-gray-100"
+ ? `${getThemeClasses("bg-accent-light")} ${getThemeClasses("text-accent")}`
+ : ""
}`}
>
{category}
-
+
))}
{/* Emoji Grid */}
-
+
{activeEmojiCategory}
-
+
{EMOJI_CATEGORIES[activeEmojiCategory].map((emoji, index) => (
- handleSelect(emoji)}
- className={`p-2 text-2xl rounded-lg hover:bg-gray-100 transition-colors ${
- value === emoji ? "bg-red-100 ring-2 ring-red-500" : ""
+ variant="ghost"
+ size="sm"
+ className={`p-2 text-2xl ${
+ safeValue === emoji ? `${getThemeClasses("bg-accent-light")} ring-2 ${getThemeClasses("ring-accent")}` : ""
}`}
title={emoji}
>
{emoji}
-
+
))}
) : (
-
+
{PREDEFINED_ICONS.map(({ id, Icon, label }) => (
- handleSelect(`icon:${id}`)}
- className={`flex flex-col items-center p-3 rounded-lg hover:bg-gray-100 transition-colors ${
- selectedIconId === id ? "bg-red-100 ring-2 ring-red-500" : ""
+ variant="ghost"
+ className={`flex flex-col items-center p-3 h-auto ${
+ selectedIconId === id ? `${getThemeClasses("bg-accent-light")} ring-2 ${getThemeClasses("ring-accent")}` : ""
}`}
title={label}
>
-
-
+
+
{label}
-
+
))}
)}
{/* Footer */}
-
-
+
-
Reset to Default
-
-
+
Cancel
-
+
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 (