diff --git a/apps/desktop/CLAUDE.md b/apps/desktop/CLAUDE.md index a0ddf2220c6..904ffffd6ca 100644 --- a/apps/desktop/CLAUDE.md +++ b/apps/desktop/CLAUDE.md @@ -1,7 +1,11 @@ +# Implementation details For Electron interprocess communnication, ALWAYS use trpc as defined in `src/lib/trpc` Please use alias as defined in `tsconfig.json` when possible +Prefer zustand for state management if it makes sense. Do not use effect unless absolutely necessary. -Please follow the clean code rules: + + +# Code quality ``` Code is clean if it can be understood easily – by everyone on the team. Clean code can be read and enhanced by a developer other than its original author. With understandability comes readability, changeability, extensibility and maintainability. _____________________________________ diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 916a6754b20..889c4e1346d 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -7,7 +7,7 @@ "resources": "src/resources", "author": { "name": "Superset", - "email": "hi@superset.com" + "email": "hi@superset.sh" }, "scripts": { "start": "electron-vite preview", @@ -48,20 +48,21 @@ "framer-motion": "^12.23.24", "http-proxy": "^1.18.1", "lowdb": "^7.0.1", - "lucide-react": "^0.553.0", "node-pty": "1.1.0-beta30", "react": "^19.1.1", "react-arborist": "^3.4.3", "react-dnd": "^16.0.1", "react-dnd-html5-backend": "^16.0.1", "react-dom": "^19.1.1", + "react-icons": "^5.5.0", "react-mosaic-component": "^6.1.1", "react-resizable-panels": "^3.0.6", "react-router-dom": "^7.8.2", "react-syntax-highlighter": "^16.1.0", "superjson": "^2.2.5", "tailwind-merge": "^2.6.0", - "trpc-electron": "^0.1.2" + "trpc-electron": "^0.1.2", + "zustand": "^5.0.8" }, "devDependencies": { "@biomejs/biome": "^2.2.6", diff --git a/apps/desktop/src/renderer/components/ZustandExample.tsx b/apps/desktop/src/renderer/components/ZustandExample.tsx new file mode 100644 index 00000000000..81feb9fc12f --- /dev/null +++ b/apps/desktop/src/renderer/components/ZustandExample.tsx @@ -0,0 +1,69 @@ +import { useCount, useExampleStore, useText } from "renderer/stores"; + +/** + * Example component demonstrating Zustand usage patterns + * + * This component shows: + * 1. Using selector hooks for optimized re-renders + * 2. Accessing actions from the store + * 3. Direct store access for multiple values + */ +export function ZustandExample() { + // Optimized selectors - only re-renders when specific values change + const count = useCount(); + const text = useText(); + + // Access actions directly from the store + const increment = useExampleStore((state) => state.increment); + const decrement = useExampleStore((state) => state.decrement); + const setText = useExampleStore((state) => state.setText); + const reset = useExampleStore((state) => state.reset); + + return ( +
+

Zustand Example

+ + {/* Counter Example */} +
+

Counter: {count}

+
+ + +
+
+ + {/* Text Input Example */} +
+

Text: {text}

+ setText(e.target.value)} + placeholder="Type something..." + className="px-3 py-2 border rounded w-full" + /> +
+ + {/* Reset Button */} + +
+ ); +} diff --git a/apps/desktop/src/renderer/lib/utils.ts b/apps/desktop/src/renderer/lib/utils.ts deleted file mode 100644 index ac680b303c9..00000000000 --- a/apps/desktop/src/renderer/lib/utils.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { type ClassValue, clsx } from "clsx"; -import { twMerge } from "tailwind-merge"; - -export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)); -} diff --git a/apps/desktop/src/renderer/screens/main/components/Sidebar/Sidebar.tsx b/apps/desktop/src/renderer/screens/main/components/Sidebar/Sidebar.tsx deleted file mode 100644 index af6ee29d7fe..00000000000 --- a/apps/desktop/src/renderer/screens/main/components/Sidebar/Sidebar.tsx +++ /dev/null @@ -1,31 +0,0 @@ -export function Sidebar() { - return ( - - ); -} diff --git a/apps/desktop/src/renderer/screens/main/components/Sidebar/index.ts b/apps/desktop/src/renderer/screens/main/components/Sidebar/index.ts deleted file mode 100644 index 7ad39439cfb..00000000000 --- a/apps/desktop/src/renderer/screens/main/components/Sidebar/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { Sidebar } from "./Sidebar"; diff --git a/apps/desktop/src/renderer/screens/main/components/Sidebar/index.tsx b/apps/desktop/src/renderer/screens/main/components/Sidebar/index.tsx new file mode 100644 index 00000000000..98519f47cf0 --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/Sidebar/index.tsx @@ -0,0 +1,65 @@ +import { motion } from "framer-motion"; +import { useSidebarStore } from "renderer/stores"; + +export function Sidebar() { + const { isSidebarOpen } = useSidebarStore(); + + return ( + + + + + + + + ); +} diff --git a/apps/desktop/src/renderer/screens/main/components/TopBar/SidebarControl.tsx b/apps/desktop/src/renderer/screens/main/components/TopBar/SidebarControl.tsx new file mode 100644 index 00000000000..8eb7b431af3 --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/TopBar/SidebarControl.tsx @@ -0,0 +1,23 @@ +import { Button } from "@superset/ui/button"; +import { HiMiniBars3, HiMiniBars3BottomLeft } from "react-icons/hi2"; +import { useSidebarStore } from "renderer/stores"; + +export function SidebarControl() { + const { isSidebarOpen, toggleSidebar } = useSidebarStore(); + + return ( + + ); +} diff --git a/apps/desktop/src/renderer/screens/main/components/TopBar/Tabs/AddTabButton.tsx b/apps/desktop/src/renderer/screens/main/components/TopBar/Tabs/AddTabButton.tsx new file mode 100644 index 00000000000..51f9946bb72 --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/TopBar/Tabs/AddTabButton.tsx @@ -0,0 +1,18 @@ +import { Button } from "@superset/ui/button"; +import { useTabsStore } from "renderer/stores/tabs"; + +export function AddTabButton() { + const { addTab } = useTabsStore(); + + return ( + + ); +} diff --git a/apps/desktop/src/renderer/screens/main/components/TopBar/Tabs/TabItem.tsx b/apps/desktop/src/renderer/screens/main/components/TopBar/Tabs/TabItem.tsx new file mode 100644 index 00000000000..064c74aa84c --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/TopBar/Tabs/TabItem.tsx @@ -0,0 +1,102 @@ +import { Button } from "@superset/ui/button"; +import { cn } from "@superset/ui/utils"; +import { useDrag, useDrop } from "react-dnd"; +import { HiMiniXMark } from "react-icons/hi2"; +import { useTabsStore } from "renderer/stores/tabs"; + +const TAB_TYPE = "TAB"; + +interface TabItemProps { + id: string; + title: string; + isActive: boolean; + index: number; + width: number; + onMouseEnter?: () => void; + onMouseLeave?: () => void; +} + +export function TabItem({ + id, + title, + isActive, + index, + width, + onMouseEnter, + onMouseLeave, +}: TabItemProps) { + const { setActiveTab, removeTab, reorderTabs } = useTabsStore(); + + const [{ isDragging }, drag] = useDrag( + () => ({ + type: TAB_TYPE, + item: { id, index }, + collect: (monitor) => ({ + isDragging: monitor.isDragging(), + }), + }), + [id, index], + ); + + const [, drop] = useDrop({ + accept: TAB_TYPE, + hover: (item: { id: string; index: number }) => { + if (item.index !== index) { + reorderTabs(item.index, index); + item.index = index; + } + }, + }); + + return ( +
+ {/* Active tab bottom border overlay */} + {isActive &&
} + + {/* Main tab button */} + + + +
+ ); +} diff --git a/apps/desktop/src/renderer/screens/main/components/TopBar/Tabs/index.tsx b/apps/desktop/src/renderer/screens/main/components/TopBar/Tabs/index.tsx new file mode 100644 index 00000000000..39ef8a7de36 --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/TopBar/Tabs/index.tsx @@ -0,0 +1,121 @@ +import { Separator } from "@superset/ui/separator"; +import { Fragment, useEffect, useRef, useState } from "react"; +import { useTabsStore } from "renderer/stores/tabs"; +import { AddTabButton } from "./AddTabButton"; +import { TabItem } from "./TabItem"; + +const MIN_TAB_WIDTH = 60; +const MAX_TAB_WIDTH = 240; +const ADD_BUTTON_WIDTH = 48; + +export function Tabs() { + const { tabs, activeTabId } = useTabsStore(); + const containerRef = useRef(null); + const scrollRef = useRef(null); + const [showStartFade, setShowStartFade] = useState(false); + const [showEndFade, setShowEndFade] = useState(false); + const [tabWidth, setTabWidth] = useState(MAX_TAB_WIDTH); + const [hoveredTabId, setHoveredTabId] = useState(null); + + useEffect(() => { + const checkScroll = () => { + if (!scrollRef.current) return; + + const { scrollLeft, scrollWidth, clientWidth } = scrollRef.current; + setShowStartFade(scrollLeft > 0); + setShowEndFade(scrollLeft < scrollWidth - clientWidth - 1); + }; + + const updateTabWidth = () => { + if (!containerRef.current) return; + + const containerWidth = containerRef.current.offsetWidth; + const availableWidth = containerWidth - ADD_BUTTON_WIDTH; + + // Calculate width: fill available space but respect min/max + const calculatedWidth = Math.max( + MIN_TAB_WIDTH, + Math.min(MAX_TAB_WIDTH, availableWidth / tabs.length), + ); + setTabWidth(calculatedWidth); + }; + + checkScroll(); + updateTabWidth(); + + const scrollElement = scrollRef.current; + if (scrollElement) { + scrollElement.addEventListener("scroll", checkScroll); + } + + window.addEventListener("resize", updateTabWidth); + + return () => { + if (scrollElement) { + scrollElement.removeEventListener("scroll", checkScroll); + } + window.removeEventListener("resize", updateTabWidth); + }; + }, [tabs]); + + return ( +
+
+
+ {tabs.map((tab, index) => { + const nextTab = tabs[index + 1]; + const isActive = tab.id === activeTabId; + const isNextActive = nextTab?.id === activeTabId; + const isHovered = tab.id === hoveredTabId; + const isNextHovered = nextTab?.id === hoveredTabId; + const separatorOpacity = + !isActive && !isNextActive && !isHovered && !isNextHovered + ? 100 + : 0; + + return ( + +
+ setHoveredTabId(tab.id)} + onMouseLeave={() => setHoveredTabId(null)} + /> +
+ {index < tabs.length - 1 && ( +
+ +
+ )} +
+ ); + })} +
+ + {/* Fade effects for scroll indication */} + {showStartFade && ( +
+ )} + {showEndFade && ( +
+ )} +
+ + +
+ ); +} diff --git a/apps/desktop/src/renderer/screens/main/components/TopBar/WindowControls.tsx b/apps/desktop/src/renderer/screens/main/components/TopBar/WindowControls.tsx index 555813d8e66..272bdd77acb 100644 --- a/apps/desktop/src/renderer/screens/main/components/TopBar/WindowControls.tsx +++ b/apps/desktop/src/renderer/screens/main/components/TopBar/WindowControls.tsx @@ -1,4 +1,4 @@ -import { Minus, Square, X } from "lucide-react"; +import { HiMiniMinus, HiMiniStop, HiMiniXMark } from "react-icons/hi2"; import { trpc } from "renderer/lib/trpc"; export function WindowControls() { @@ -25,21 +25,21 @@ export function WindowControls() { className="h-full w-12 flex items-center justify-center hover:bg-accent transition-colors" onClick={handleMinimize} > - +
); diff --git a/apps/desktop/src/renderer/screens/main/components/TopBar/index.ts b/apps/desktop/src/renderer/screens/main/components/TopBar/index.ts deleted file mode 100644 index 7cc3ff0450b..00000000000 --- a/apps/desktop/src/renderer/screens/main/components/TopBar/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { TopBar } from "./TopBar"; diff --git a/apps/desktop/src/renderer/screens/main/components/TopBar/TopBar.tsx b/apps/desktop/src/renderer/screens/main/components/TopBar/index.tsx similarity index 60% rename from apps/desktop/src/renderer/screens/main/components/TopBar/TopBar.tsx rename to apps/desktop/src/renderer/screens/main/components/TopBar/index.tsx index d1141845867..12814cf85c9 100644 --- a/apps/desktop/src/renderer/screens/main/components/TopBar/TopBar.tsx +++ b/apps/desktop/src/renderer/screens/main/components/TopBar/index.tsx @@ -1,20 +1,24 @@ import { trpc } from "renderer/lib/trpc"; +import { SidebarControl } from "./SidebarControl"; +import { Tabs } from "./Tabs"; import { WindowControls } from "./WindowControls"; export function TopBar() { const { data: platform } = trpc.window.getPlatform.useQuery(); const isMac = platform === "darwin"; return ( -
+
-

Superset

+ +
+
+
-
Middle section
{!isMac && }
diff --git a/apps/desktop/src/renderer/stores/README.md b/apps/desktop/src/renderer/stores/README.md new file mode 100644 index 00000000000..e9aa8c2caed --- /dev/null +++ b/apps/desktop/src/renderer/stores/README.md @@ -0,0 +1,191 @@ +# Zustand Stores + +This directory contains Zustand stores for React state management in the renderer process. + +## Quick Start + +```typescript +// 1. Create a store +import { create } from 'zustand'; +import { devtools } from 'zustand/middleware'; + +interface MyState { + value: string; + setValue: (value: string) => void; +} + +export const useMyStore = create()( + devtools( + (set) => ({ + value: '', + setValue: (value) => set({ value }), + }), + { name: 'MyStore' } + ) +); + +// 2. Use in component +import { useMyStore } from 'renderer/stores'; + +function MyComponent() { + const value = useMyStore((state) => state.value); + const setValue = useMyStore((state) => state.setValue); + + return setValue(e.target.value)} />; +} +``` + +## Best Practices + +### 1. TypeScript First +Always define interfaces for your stores: +```typescript +interface UserState { + user: User | null; + setUser: (user: User) => void; + clearUser: () => void; +} +``` + +### 2. Use Selectors for Performance +Create selector hooks to prevent unnecessary re-renders: +```typescript +// In store file +export const useUser = () => useUserStore((state) => state.user); +export const useUserName = () => useUserStore((state) => state.user?.name); + +// In component - only re-renders when name changes +const userName = useUserName(); +``` + +### 3. DevTools Integration +Always use devtools middleware in development: +```typescript +export const useMyStore = create()( + devtools( + (set) => ({ /* store definition */ }), + { name: 'MyStore' } // Shows in Redux DevTools + ) +); +``` + +### 4. Persistence (Optional) +Use persist middleware for localStorage persistence: +```typescript +import { persist } from 'zustand/middleware'; + +export const useMyStore = create()( + devtools( + persist( + (set) => ({ /* store definition */ }), + { name: 'my-store-key' } // localStorage key + ), + { name: 'MyStore' } + ) +); +``` + +### 5. Organize Actions +Group related actions together: +```typescript +export const useTaskStore = create()( + devtools((set) => ({ + tasks: [], + + // Add actions + addTask: (task) => set((state) => ({ + tasks: [...state.tasks, task] + })), + + // Update actions + updateTask: (id, updates) => set((state) => ({ + tasks: state.tasks.map(t => t.id === id ? { ...t, ...updates } : t) + })), + + // Remove actions + removeTask: (id) => set((state) => ({ + tasks: state.tasks.filter(t => t.id !== id) + })), + + // Bulk actions + clearCompleted: () => set((state) => ({ + tasks: state.tasks.filter(t => !t.completed) + })), + })) +); +``` + +### 6. Async Actions +Handle async operations within actions: +```typescript +export const useDataStore = create()( + devtools((set) => ({ + data: null, + loading: false, + error: null, + + fetchData: async () => { + set({ loading: true, error: null }); + try { + const data = await api.getData(); + set({ data, loading: false }); + } catch (error) { + set({ error, loading: false }); + } + }, + })) +); +``` + +### 7. Slice Pattern for Large Stores +Split large stores into slices: +```typescript +interface UserSlice { + user: User | null; + setUser: (user: User) => void; +} + +interface SettingsSlice { + theme: 'light' | 'dark'; + setTheme: (theme: 'light' | 'dark') => void; +} + +type AppState = UserSlice & SettingsSlice; + +const createUserSlice = (set): UserSlice => ({ + user: null, + setUser: (user) => set({ user }), +}); + +const createSettingsSlice = (set): SettingsSlice => ({ + theme: 'light', + setTheme: (theme) => set({ theme }), +}); + +export const useAppStore = create()( + devtools( + (...a) => ({ + ...createUserSlice(...a), + ...createSettingsSlice(...a), + }) + ) +); +``` + +## When to Use Zustand vs tRPC + +- **Zustand**: Local UI state (sidebar open/closed, active tab, theme preferences) +- **tRPC**: Server state and IPC communication with Electron main process +- **Combine them**: Use tRPC queries to fetch data, store UI state in Zustand + +## Debugging + +1. Install Redux DevTools browser extension +2. Stores with `devtools()` middleware will appear in the extension +3. You can inspect state changes, time-travel, and dispatch actions + +## Resources + +- [Zustand Documentation](https://zustand.docs.pmnd.rs/) +- [TypeScript Guide](https://zustand.docs.pmnd.rs/guides/typescript) +- [Persisting Store](https://zustand.docs.pmnd.rs/integrations/persisting-store-data) diff --git a/apps/desktop/src/renderer/stores/example-store.ts b/apps/desktop/src/renderer/stores/example-store.ts new file mode 100644 index 00000000000..38567b35339 --- /dev/null +++ b/apps/desktop/src/renderer/stores/example-store.ts @@ -0,0 +1,54 @@ +import { create } from "zustand"; +import { devtools } from "zustand/middleware"; + +/** + * Example store demonstrating Zustand usage patterns + * + * Features: + * - TypeScript type safety + * - DevTools integration for debugging + * - Optional persistence (commented out by default) + */ + +interface ExampleState { + // State + count: number; + text: string; + + // Actions + increment: () => void; + decrement: () => void; + setText: (text: string) => void; + reset: () => void; +} + +const initialState = { + count: 0, + text: "", +}; + +export const useExampleStore = create()( + devtools( + // Uncomment persist middleware if you need localStorage persistence + // persist( + (set) => ({ + ...initialState, + + increment: () => set((state) => ({ count: state.count + 1 })), + decrement: () => set((state) => ({ count: state.count - 1 })), + setText: (text: string) => set({ text }), + reset: () => set(initialState), + }), + // { + // name: "example-store", // localStorage key + // } + // ), + { + name: "ExampleStore", // DevTools name + }, + ), +); + +// Selectors for optimized re-renders +export const useCount = () => useExampleStore((state) => state.count); +export const useText = () => useExampleStore((state) => state.text); diff --git a/apps/desktop/src/renderer/stores/index.ts b/apps/desktop/src/renderer/stores/index.ts new file mode 100644 index 00000000000..7443ba32acf --- /dev/null +++ b/apps/desktop/src/renderer/stores/index.ts @@ -0,0 +1,8 @@ +/** + * Zustand stores index + * + * Export all stores from this file for easy importing + */ + +export * from "./sidebar-state"; +export * from "./tabs"; diff --git a/apps/desktop/src/renderer/stores/sidebar-state.ts b/apps/desktop/src/renderer/stores/sidebar-state.ts new file mode 100644 index 00000000000..36c276a8dc7 --- /dev/null +++ b/apps/desktop/src/renderer/stores/sidebar-state.ts @@ -0,0 +1,25 @@ +import { create } from "zustand"; +import { devtools } from "zustand/middleware"; + +interface SidebarState { + isSidebarOpen: boolean; + toggleSidebar: () => void; + setSidebarOpen: (open: boolean) => void; +} + +export const useSidebarStore = create()( + devtools( + (set) => ({ + isSidebarOpen: true, + + toggleSidebar: () => { + set((state) => ({ isSidebarOpen: !state.isSidebarOpen })); + }, + + setSidebarOpen: (open) => { + set({ isSidebarOpen: open }); + }, + }), + { name: "SidebarStore" }, + ), +); diff --git a/apps/desktop/src/renderer/stores/tabs.ts b/apps/desktop/src/renderer/stores/tabs.ts new file mode 100644 index 00000000000..e1dc5fe8cf4 --- /dev/null +++ b/apps/desktop/src/renderer/stores/tabs.ts @@ -0,0 +1,71 @@ +import { create } from "zustand"; +import { devtools } from "zustand/middleware"; + +export interface Tab { + id: string; + title: string; +} + +interface TabsState { + tabs: Tab[]; + activeTabId: string | null; + + addTab: () => void; + removeTab: (id: string) => void; + setActiveTab: (id: string) => void; + reorderTabs: (startIndex: number, endIndex: number) => void; +} + +const createNewTab = (): Tab => ({ + id: `tab-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`, + title: "New Tab", +}); + +export const useTabsStore = create()( + devtools( + (set) => ({ + tabs: [{ id: "tab-1", title: "Home" }], + activeTabId: "tab-1", + + addTab: () => { + const newTab = createNewTab(); + set((state) => ({ + tabs: [...state.tabs, newTab], + activeTabId: newTab.id, + })); + }, + + removeTab: (id) => { + set((state) => { + const tabs = state.tabs.filter((tab) => tab.id !== id); + if (tabs.length === 0) { + const newTab = createNewTab(); + return { tabs: [newTab], activeTabId: newTab.id }; + } + + if (id === state.activeTabId) { + const closedIndex = state.tabs.findIndex((tab) => tab.id === id); + const nextTab = tabs[closedIndex] || tabs[closedIndex - 1]; + return { tabs, activeTabId: nextTab.id }; + } + + return { tabs }; + }); + }, + + setActiveTab: (id) => { + set({ activeTabId: id }); + }, + + reorderTabs: (startIndex, endIndex) => { + set((state) => { + const tabs = [...state.tabs]; + const [removed] = tabs.splice(startIndex, 1); + tabs.splice(endIndex, 0, removed); + return { tabs }; + }); + }, + }), + { name: "TabsStore" }, + ), +); diff --git a/apps/desktop/src/shared/ipc-channels/index.ts b/apps/desktop/src/shared/ipc-channels/index.ts index 02d1ae0acd6..7eb575807cf 100644 --- a/apps/desktop/src/shared/ipc-channels/index.ts +++ b/apps/desktop/src/shared/ipc-channels/index.ts @@ -12,8 +12,8 @@ import type { TabChannels } from "./tab"; import type { TerminalChannels } from "./terminal"; import type { UiChannels } from "./ui"; import type { WindowChannels } from "./window"; -import type { WorktreeChannels } from "./worktree"; import type { WorkspaceChannels } from "./workspace"; +import type { WorktreeChannels } from "./worktree"; // Re-export shared types export type { diff --git a/apps/desktop/src/shared/ipc-channels/ui.ts b/apps/desktop/src/shared/ipc-channels/ui.ts index d25823a11e5..e580c7390f2 100644 --- a/apps/desktop/src/shared/ipc-channels/ui.ts +++ b/apps/desktop/src/shared/ipc-channels/ui.ts @@ -2,8 +2,8 @@ * UI-related IPC channels for Desktop app */ +import type { MosaicNode, Tab } from "../types"; import type { IpcResponse, NoRequest } from "./types"; -import type { Tab, MosaicNode } from "../types"; export interface UiChannels { // Workspace UI state diff --git a/bun.lock b/bun.lock index a224c8b5621..e60ff3d47d9 100644 --- a/bun.lock +++ b/bun.lock @@ -94,13 +94,13 @@ "framer-motion": "^12.23.24", "http-proxy": "^1.18.1", "lowdb": "^7.0.1", - "lucide-react": "^0.553.0", "node-pty": "1.1.0-beta30", "react": "^19.1.1", "react-arborist": "^3.4.3", "react-dnd": "^16.0.1", "react-dnd-html5-backend": "^16.0.1", "react-dom": "^19.1.1", + "react-icons": "^5.5.0", "react-mosaic-component": "^6.1.1", "react-resizable-panels": "^3.0.6", "react-router-dom": "^7.8.2", @@ -108,6 +108,7 @@ "superjson": "^2.2.5", "tailwind-merge": "^2.6.0", "trpc-electron": "^0.1.2", + "zustand": "^5.0.8", }, "devDependencies": { "@biomejs/biome": "^2.2.6", @@ -355,11 +356,12 @@ "@radix-ui/react-label": "^2.1.8", "@radix-ui/react-scroll-area": "^1.2.10", "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-separator": "^1.1.8", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tooltip": "^1.2.8", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", - "lucide-react": "^0.553.0", + "react-icons": "^5.5.0", "react-resizable-panels": "^3.0.6", "tailwind-merge": "^2.6.0", }, @@ -890,6 +892,8 @@ "@radix-ui/react-select": ["@radix-ui/react-select@2.2.6", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ=="], + "@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.8", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g=="], + "@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="], "@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.2.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg=="], @@ -3066,6 +3070,8 @@ "react-dom": ["react-dom@19.2.0", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ=="], + "react-icons": ["react-icons@5.5.0", "", { "peerDependencies": { "react": "*" } }, "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw=="], + "react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], "react-medium-image-zoom": ["react-medium-image-zoom@5.4.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-BsE+EnFVQzFIlyuuQrZ9iTwyKpKkqdFZV1ImEQN573QPqGrIUuNni7aF+sZwDcxlsuOMayCr6oO/PZR/yJnbRg=="], @@ -3794,6 +3800,8 @@ "@radix-ui/react-select/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + "@radix-ui/react-separator/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="], + "@radix-ui/react-tooltip/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], "@react-three/drei/cross-env": ["cross-env@7.0.3", "", { "dependencies": { "cross-spawn": "^7.0.1" }, "bin": { "cross-env": "src/bin/cross-env.js", "cross-env-shell": "src/bin/cross-env-shell.js" } }, "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw=="], diff --git a/packages/ui/package.json b/packages/ui/package.json index eb01e898a89..c02a1404edf 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -19,6 +19,7 @@ "./resizable": "./src/components/resizable.tsx", "./label": "./src/components/label.tsx", "./select": "./src/components/select.tsx", + "./separator": "./src/components/separator.tsx", "./textarea": "./src/components/textarea.tsx", "./utils": "./src/lib/utils.ts", "./hooks": "./src/hooks/index.ts", @@ -35,11 +36,12 @@ "@radix-ui/react-label": "^2.1.8", "@radix-ui/react-scroll-area": "^1.2.10", "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-separator": "^1.1.8", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tooltip": "^1.2.8", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", - "lucide-react": "^0.553.0", + "react-icons": "^5.5.0", "react-resizable-panels": "^3.0.6", "tailwind-merge": "^2.6.0" }, diff --git a/packages/ui/src/components/context-menu.tsx b/packages/ui/src/components/context-menu.tsx index d186942f0b1..53a55fb9c57 100644 --- a/packages/ui/src/components/context-menu.tsx +++ b/packages/ui/src/components/context-menu.tsx @@ -1,5 +1,5 @@ import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"; -import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"; +import { HiMiniCheck, HiMiniChevronRight, HiMiniStop } from "react-icons/hi2"; import type * as React from "react"; import { cn } from "../lib/utils"; @@ -76,7 +76,7 @@ function ContextMenuSubTrigger({ {...props} > {children} - + ); } @@ -156,7 +156,7 @@ function ContextMenuCheckboxItem({ > - + {children} @@ -180,7 +180,7 @@ function ContextMenuRadioItem({ > - + {children} diff --git a/packages/ui/src/components/dialog.tsx b/packages/ui/src/components/dialog.tsx index 20a846728c9..0680ed61b4f 100644 --- a/packages/ui/src/components/dialog.tsx +++ b/packages/ui/src/components/dialog.tsx @@ -1,5 +1,5 @@ import * as DialogPrimitive from "@radix-ui/react-dialog"; -import { XIcon } from "lucide-react"; +import { HiMiniXMark } from "react-icons/hi2"; import type * as React from "react"; import { cn } from "../lib/utils"; @@ -70,7 +70,7 @@ function DialogContent({ data-slot="dialog-close" className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4" > - + Close )} diff --git a/packages/ui/src/components/dropdown-menu.tsx b/packages/ui/src/components/dropdown-menu.tsx index 9a1ea6a2752..43d40d3794c 100644 --- a/packages/ui/src/components/dropdown-menu.tsx +++ b/packages/ui/src/components/dropdown-menu.tsx @@ -1,5 +1,5 @@ import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"; -import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"; +import { HiMiniCheck, HiMiniChevronRight, HiMiniStop } from "react-icons/hi2"; import type * as React from "react"; import { cn } from "../lib/utils"; @@ -103,7 +103,7 @@ function DropdownMenuCheckboxItem({ > - + {children} @@ -138,7 +138,7 @@ function DropdownMenuRadioItem({ > - + {children} @@ -220,7 +220,7 @@ function DropdownMenuSubTrigger({ {...props} > {children} - + ); } diff --git a/packages/ui/src/components/resizable.tsx b/packages/ui/src/components/resizable.tsx index 8dc97d79ea7..a4225e4cad4 100644 --- a/packages/ui/src/components/resizable.tsx +++ b/packages/ui/src/components/resizable.tsx @@ -1,4 +1,4 @@ -import { GripVerticalIcon } from "lucide-react"; +import { HiMiniEllipsisVertical } from "react-icons/hi2"; import type * as React from "react"; import * as ResizablePrimitive from "react-resizable-panels"; @@ -44,7 +44,7 @@ function ResizableHandle({ > {withHandle && (
- +
)} diff --git a/packages/ui/src/components/select.tsx b/packages/ui/src/components/select.tsx index 65d9c6b2750..205251792d5 100644 --- a/packages/ui/src/components/select.tsx +++ b/packages/ui/src/components/select.tsx @@ -1,7 +1,7 @@ "use client"; import * as SelectPrimitive from "@radix-ui/react-select"; -import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"; +import { HiMiniCheck, HiMiniChevronDown, HiMiniChevronUp } from "react-icons/hi2"; import type * as React from "react"; import { cn } from "../lib/utils"; @@ -44,7 +44,7 @@ function SelectTrigger({ > {children} - + ); @@ -116,7 +116,7 @@ function SelectItem({ > - + {children} @@ -150,7 +150,7 @@ function SelectScrollUpButton({ )} {...props} > - + ); } @@ -168,7 +168,7 @@ function SelectScrollDownButton({ )} {...props} > - + ); } diff --git a/packages/ui/src/components/separator.tsx b/packages/ui/src/components/separator.tsx new file mode 100644 index 00000000000..d57e7e2272f --- /dev/null +++ b/packages/ui/src/components/separator.tsx @@ -0,0 +1,25 @@ +import * as React from "react"; +import * as SeparatorPrimitive from "@radix-ui/react-separator"; +import { cn } from "../lib/utils"; + +function Separator({ + className, + orientation = "horizontal", + decorative = true, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export { Separator };