diff --git a/components.json b/components.json
index 283cb015f5..3d64f95424 100644
--- a/components.json
+++ b/components.json
@@ -11,7 +11,7 @@
"prefix": ""
},
"aliases": {
- "components": "@renderer",
+ "components": "@renderer/ui/third-party",
"utils": "@renderer/utils",
"ui": "@renderer/ui",
"lib": "@renderer/lib",
diff --git a/package.json b/package.json
index 1e3ca06b95..8694b7210e 100644
--- a/package.json
+++ b/package.json
@@ -188,6 +188,7 @@
"lru-cache": "^11.1.0",
"lucide-react": "^0.509.0",
"mime": "^4.0.4",
+ "motion": "^12.11.0",
"npx-scope-finder": "^1.2.0",
"openai": "patch:openai@npm%3A4.96.0#~/.yarn/patches/openai-npm-4.96.0-0665b05cb9.patch",
"p-queue": "^8.1.0",
diff --git a/src/renderer/src/assets/styles/index.scss b/src/renderer/src/assets/styles/index.scss
index 9721f36126..baf9bda8c6 100644
--- a/src/renderer/src/assets/styles/index.scss
+++ b/src/renderer/src/assets/styles/index.scss
@@ -136,7 +136,7 @@ body[theme-mode='light'] {
*::before,
*::after {
box-sizing: border-box;
- margin: 0;
+ // margin: 0;
font-weight: normal;
}
diff --git a/src/renderer/src/assets/styles/tailwind.css b/src/renderer/src/assets/styles/tailwind.css
index f25c11060c..f161b48776 100644
--- a/src/renderer/src/assets/styles/tailwind.css
+++ b/src/renderer/src/assets/styles/tailwind.css
@@ -109,6 +109,24 @@
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
+ --animate-marquee: marquee var(--duration) infinite linear;
+ --animate-marquee-vertical: marquee-vertical var(--duration) linear infinite;
+ @keyframes marquee {
+ from {
+ transform: translateX(0);
+ }
+ to {
+ transform: translateX(calc(-100% - var(--gap)));
+ }
+ }
+ @keyframes marquee-vertical {
+ from {
+ transform: translateY(0);
+ }
+ to {
+ transform: translateY(calc(-100% - var(--gap)));
+ }
+ }
}
@layer base {
@@ -118,4 +136,4 @@
body {
@apply bg-background text-foreground;
}
-}
+}
\ No newline at end of file
diff --git a/src/renderer/src/components/app/Sidebar.tsx b/src/renderer/src/components/app/Sidebar.tsx
index 7f58de9f5f..60fbe2d608 100644
--- a/src/renderer/src/components/app/Sidebar.tsx
+++ b/src/renderer/src/components/app/Sidebar.tsx
@@ -11,8 +11,8 @@ import { isEmoji } from '@renderer/utils'
import type { MenuProps } from 'antd'
import { Avatar, Dropdown, Tooltip } from 'antd'
import {
- Box,
CircleHelp,
+ Compass,
FileSearch,
Folder,
Languages,
@@ -145,7 +145,7 @@ const MainMenus: FC = () => {
minapp: ,
knowledge: ,
files: ,
- store:
+ store:
}
const pathMap = {
diff --git a/src/renderer/src/pages/store/components/StoreContent.tsx b/src/renderer/src/pages/store/components/StoreContent.tsx
new file mode 100644
index 0000000000..4bd11e03f1
--- /dev/null
+++ b/src/renderer/src/pages/store/components/StoreContent.tsx
@@ -0,0 +1,207 @@
+import { Badge } from '@renderer/ui/badge'
+import { Button } from '@renderer/ui/button'
+import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '@renderer/ui/card'
+import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@renderer/ui/dropdown-menu'
+import { Input } from '@renderer/ui/input'
+import { BlurFade } from '@renderer/ui/third-party/BlurFade'
+import { cn } from '@renderer/utils'
+import { Download, Filter, Grid3X3, List, Search, Star } from 'lucide-react'
+// Define the type for a store item based on store_list.json
+interface StoreItem {
+ id: number
+ title: string
+ description: string
+ type: string
+ categoryId: string
+ subcategoryId: string
+ author: string
+ rating: number
+ downloads: string
+ image?: string
+ tags: string[]
+ featured: boolean
+}
+
+interface StoreContentProps {
+ viewMode: 'grid' | 'list'
+ searchQuery: string
+ selectedCategory: string
+ items: StoreItem[]
+ onSearchQueryChange: (query: string) => void
+ onViewModeChange: (mode: 'grid' | 'list') => void
+}
+
+export function StoreContent({
+ viewMode,
+ searchQuery,
+ items,
+ onSearchQueryChange,
+ onViewModeChange
+}: StoreContentProps) {
+ return (
+
+ {/* Sticky Header for Search, Filter, View Mode, and Category Tabs */}
+
+
+ {/* Top row: Search, Filter, View buttons */}
+
+
+
+ onSearchQueryChange(e.target.value)}
+ />
+
+
+
+
+
+
+ {/* Add actual filtering logic later */}
+ Most Popular
+ Newest
+ Highest Rated
+
+
+
+
+
+
+ {/* Bottom row: Category Tabs */}
+ {/*
+
+ All
+ MCP Services
+ Plugins
+ Applications
+
+ */}
+
+
+
+ {/* Main Content Area: Grid or List View */}
+
+ {items.length === 0 ? (
+
+
No items found matching your criteria.
+
+ ) : viewMode === 'grid' ? (
+
+ ) : (
+
+ )}
+
+
+ )
+}
+
+// Grid View Component
+function GridView({ items }: { items: StoreItem[] }) {
+ return (
+
+ {items.map((item, idx) => (
+
+
+
+
+

+
+
+
+
+
+
{item.title}
+
{item.author}
+
+
{item.type}
+
+ {item.description}
+
+
+
+
+ {item.rating}
+
+
+
+
+
+ ))}
+
+ )
+}
+
+// List View Component
+function ListView({ items }: { items: StoreItem[] }) {
+ return (
+
+ {items.map((item) => (
+
+
+
+

+
+
+
+
+
+
{item.title}
+
{item.author}
+
+
{item.type}
+
+
{item.description}
+
+ {item.tags.map((tag) => (
+
+ {tag}
+
+ ))}
+
+
+
+
+
+
+ {item.rating}
+ ({item.downloads})
+
+
+
+
+
+ ))}
+
+ )
+}
diff --git a/src/renderer/src/pages/store/components/StoreSidebar.tsx b/src/renderer/src/pages/store/components/StoreSidebar.tsx
new file mode 100644
index 0000000000..6898f62bb0
--- /dev/null
+++ b/src/renderer/src/pages/store/components/StoreSidebar.tsx
@@ -0,0 +1,57 @@
+import { Badge } from '@renderer/ui/badge'
+import {
+ Sidebar,
+ SidebarContent,
+ SidebarGroup,
+ SidebarGroupContent,
+ SidebarGroupLabel,
+ SidebarMenu,
+ SidebarMenuButton,
+ SidebarMenuItem
+} from '@renderer/ui/sidebar'
+
+import categories from '../data/store_categories.json'
+
+interface StoreSidebarProps {
+ selectedCategory: string
+ selectedSubcategory: string
+ onSelectCategory: (categoryId: string, subcategoryId: string) => void
+}
+
+export function StoreSidebar({ selectedCategory, selectedSubcategory, onSelectCategory }: StoreSidebarProps) {
+ console.log('selectedCategory', selectedCategory)
+ console.log('selectedSubcategory', selectedSubcategory)
+ return (
+
+
+ {categories.map((category) => (
+
+ {/* Only render label if it's not the 'all' category wrapper */}
+ {category.id !== 'all' && {category.title}}
+
+
+ {category.items.map((item) => (
+
+ {
+ console.log('category', category)
+ // Special handling for top-level 'all' categories vs nested ones
+ onSelectCategory(category.id, item.id) // Selecting 'all', 'featured', 'new', 'top' uses the item.id as category
+ }}>
+ {item.name}
+
+ {item.count}
+
+
+
+ ))}
+
+
+
+ ))}
+
+
+ )
+}
diff --git a/resources/data/store_categories.json b/src/renderer/src/pages/store/data/store_categories.json
similarity index 100%
rename from resources/data/store_categories.json
rename to src/renderer/src/pages/store/data/store_categories.json
diff --git a/resources/data/store_list.json b/src/renderer/src/pages/store/data/store_list.json
similarity index 100%
rename from resources/data/store_list.json
rename to src/renderer/src/pages/store/data/store_list.json
diff --git a/src/renderer/src/pages/store/index.tsx b/src/renderer/src/pages/store/index.tsx
index 43db372b7a..9af45df52d 100644
--- a/src/renderer/src/pages/store/index.tsx
+++ b/src/renderer/src/pages/store/index.tsx
@@ -1,72 +1,11 @@
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
-import { Badge } from '@renderer/ui/badge'
-import { Button } from '@renderer/ui/button'
-import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '@renderer/ui/card'
-import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@renderer/ui/dropdown-menu'
-import { Input } from '@renderer/ui/input'
-import {
- Sidebar,
- SidebarContent,
- SidebarGroup,
- SidebarGroupContent,
- SidebarGroupLabel,
- SidebarHeader,
- SidebarMenu,
- SidebarMenuButton,
- SidebarMenuItem,
- SidebarProvider
-} from '@renderer/ui/sidebar'
-import { Tabs, TabsList, TabsTrigger } from '@renderer/ui/tabs'
-import { cn } from '@renderer/utils'
-import { Download, Filter, Grid3X3, List, Search, Star } from 'lucide-react'
+import { SidebarProvider } from '@renderer/ui/sidebar'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
-import categories from '../../../../../resources/data/store_categories.json'
-import storeList from '../../../../../resources/data/store_list.json'
-
-// const categories = [
-// {
-// id: 'all',
-// title: 'Categories',
-// items: [
-// { id: 'all', name: 'All', count: 120, isActive: true },
-// { id: 'featured', name: 'Featured', count: 24 },
-// { id: 'new', name: 'New Releases', count: 18 },
-// { id: 'top', name: 'Top Rated', count: 32 }
-// ]
-// },
-// {
-// id: 'mcp',
-// title: 'MCP Services',
-// items: [
-// { id: 'mcp-text', name: 'Text Generation', count: 15 },
-// { id: 'mcp-image', name: 'Image Generation', count: 8 },
-// { id: 'mcp-audio', name: 'Audio Processing', count: 6 },
-// { id: 'mcp-code', name: 'Code Assistance', count: 12 }
-// ]
-// },
-// {
-// id: 'plugins',
-// title: 'Plugins',
-// items: [
-// { id: 'plugin-productivity', name: 'Productivity', count: 14 },
-// { id: 'plugin-development', name: 'Development', count: 22 },
-// { id: 'plugin-design', name: 'Design', count: 9 },
-// { id: 'plugin-utilities', name: 'Utilities', count: 18 }
-// ]
-// },
-// {
-// id: 'apps',
-// title: 'Applications',
-// items: [
-// { id: 'app-desktop', name: 'Desktop', count: 7 },
-// { id: 'app-web', name: 'Web', count: 11 },
-// { id: 'app-mobile', name: 'Mobile', count: 5 },
-// { id: 'app-cli', name: 'CLI', count: 8 }
-// ]
-// }
-// ]
+import { StoreContent } from './components/StoreContent'
+import { StoreSidebar } from './components/StoreSidebar'
+import storeList from './data/store_list.json'
export default function StoreLayout() {
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid')
@@ -74,9 +13,8 @@ export default function StoreLayout() {
const [selectedCategory, setSelectedCategory] = useState('all')
const [selectedSubcategory, setSelectedSubcategory] = useState('all')
const { t } = useTranslation()
- // Update the filteredItems logic to use the new category IDs
+
const filteredItems = storeList.filter((item) => {
- // First apply search filter
const matchesSearch =
searchQuery === '' ||
item.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
@@ -84,17 +22,38 @@ export default function StoreLayout() {
item.author.toLowerCase().includes(searchQuery.toLowerCase()) ||
item.tags.some((tag) => tag.toLowerCase().includes(searchQuery.toLowerCase()))
- // Then apply category filters
- const matchesCategory =
- selectedCategory === 'all' ||
- item.categoryId === selectedCategory ||
- (selectedCategory === 'featured' && item.featured)
+ let matchesCategory = false
+ if (selectedCategory === 'all') {
+ matchesCategory = true
+ } else if (['featured', 'new', 'top'].includes(selectedCategory)) {
+ if (selectedCategory === 'featured') {
+ matchesCategory = item.featured === true
+ }
+ } else {
+ matchesCategory = item.categoryId === selectedCategory
+ }
- const matchesSubcategory = selectedSubcategory === 'all' || item.subcategoryId === selectedSubcategory
+ const matchesSubcategory =
+ ['all', 'featured', 'new', 'top'].includes(selectedCategory) ||
+ selectedSubcategory === 'all' ||
+ item.subcategoryId === selectedSubcategory
return matchesSearch && matchesCategory && matchesSubcategory
})
+ const handleSelectCategory = (categoryId: string, subcategoryId: string) => {
+ console.log('categoryId', categoryId)
+ console.log('subcategoryId', subcategoryId)
+ setSelectedCategory(categoryId)
+ setSelectedSubcategory(subcategoryId)
+ // setSelectedSubcategory('all')
+ }
+
+ // const handleTabCategoryChange = (categoryId: string) => {
+ // setSelectedCategory(categoryId)
+ // setSelectedSubcategory('all')
+ // }
+
return (
@@ -102,190 +61,19 @@ export default function StoreLayout() {
-
-
-
-
Cherry Store
-
-
-
- {categories.map((category) => (
-
- {category.title}
-
-
- {category.items.map((item) => (
-
- {
- setSelectedCategory(category.id)
- setSelectedSubcategory(item.id)
- }}>
- {item.name}
-
- {item.count}
-
-
-
- ))}
-
-
-
- ))}
-
-
-
-
-
-
-
-
-
- setSearchQuery(e.target.value)}
- />
-
-
-
-
-
-
- Most Popular
- Newest
- Highest Rated
-
-
-
-
-
-
-
{
- setSelectedCategory(value)
- setSelectedSubcategory('all')
- }}>
-
- All
- MCP Services
- Plugins
- Applications
-
-
-
-
-
-
- {filteredItems.length === 0 ? (
-
-
No items found matching your search.
-
- ) : viewMode === 'grid' ? (
-
- {filteredItems.map((item) => (
-
-
-
-

-
-
-
-
-
-
{item.title}
-
{item.author}
-
-
{item.type}
-
- {item.description}
-
-
-
-
- {item.rating}
-
-
-
-
- ))}
-
- ) : (
-
- {filteredItems.map((item) => (
-
-
-
-

-
-
-
-
-
-
{item.title}
-
{item.author}
-
-
{item.type}
-
-
{item.description}
-
- {item.tags.map((tag) => (
-
- {tag}
-
- ))}
-
-
-
-
-
-
- {item.rating}
- ({item.downloads})
-
-
-
-
-
- ))}
-
- )}
-
-
+
+
diff --git a/src/renderer/src/ui/alert.tsx b/src/renderer/src/ui/alert.tsx
new file mode 100644
index 0000000000..7d0a2918d6
--- /dev/null
+++ b/src/renderer/src/ui/alert.tsx
@@ -0,0 +1,66 @@
+import * as React from "react"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@renderer/utils/index"
+
+const alertVariants = cva(
+ "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
+ {
+ variants: {
+ variant: {
+ default: "bg-card text-card-foreground",
+ destructive:
+ "text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+)
+
+function Alert({
+ className,
+ variant,
+ ...props
+}: React.ComponentProps<"div"> & VariantProps) {
+ return (
+
+ )
+}
+
+function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function AlertDescription({
+ className,
+ ...props
+}: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+export { Alert, AlertTitle, AlertDescription }
diff --git a/src/renderer/src/ui/third-party/BlurFade.tsx b/src/renderer/src/ui/third-party/BlurFade.tsx
new file mode 100644
index 0000000000..ae5930cac1
--- /dev/null
+++ b/src/renderer/src/ui/third-party/BlurFade.tsx
@@ -0,0 +1,73 @@
+'use client'
+
+import { AnimatePresence, motion, MotionProps, useInView, UseInViewOptions, Variants } from 'motion/react'
+import { useRef } from 'react'
+
+type MarginType = UseInViewOptions['margin']
+
+interface BlurFadeProps extends MotionProps {
+ children: React.ReactNode
+ className?: string
+ variant?: {
+ hidden: { y: number }
+ visible: { y: number }
+ }
+ duration?: number
+ delay?: number
+ offset?: number
+ direction?: 'up' | 'down' | 'left' | 'right'
+ inView?: boolean
+ inViewMargin?: MarginType
+ blur?: string
+}
+
+export function BlurFade({
+ children,
+ className,
+ variant,
+ duration = 0.4,
+ delay = 0,
+ offset = 6,
+ direction = 'down',
+ inView = false,
+ inViewMargin = '-50px',
+ blur = '6px',
+ ...props
+}: BlurFadeProps) {
+ const ref = useRef(null)
+ const inViewResult = useInView(ref, { once: true, margin: inViewMargin })
+ const isInView = !inView || inViewResult
+ const defaultVariants: Variants = {
+ hidden: {
+ [direction === 'left' || direction === 'right' ? 'x' : 'y']:
+ direction === 'right' || direction === 'down' ? -offset : offset,
+ opacity: 0,
+ filter: `blur(${blur})`
+ },
+ visible: {
+ [direction === 'left' || direction === 'right' ? 'x' : 'y']: 0,
+ opacity: 1,
+ filter: `blur(0px)`
+ }
+ }
+ const combinedVariants = variant || defaultVariants
+ return (
+
+
+ {children}
+
+
+ )
+}
diff --git a/tsconfig.node.json b/tsconfig.node.json
index 11e2bf70de..885f343a91 100644
--- a/tsconfig.node.json
+++ b/tsconfig.node.json
@@ -18,8 +18,8 @@
"paths": {
"@main/*": ["src/main/*"],
"@types": ["src/renderer/src/types/index.ts"],
- "@components/*": ["packages/components/*"],
- "@shared/*": ["packages/shared/*"]
+ "@shared/*": ["packages/shared/*"],
+ "@renderer/*": ["src/renderer/src/*"]
}
}
}
diff --git a/yarn.lock b/yarn.lock
index f9e0d334fe..abd5141749 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5311,6 +5311,7 @@ __metadata:
lucide-react: "npm:^0.509.0"
markdown-it: "npm:^14.1.0"
mime: "npm:^4.0.4"
+ motion: "npm:^12.11.0"
next-themes: "npm:^0.4.6"
node-stream-zip: "npm:^1.15.0"
npx-scope-finder: "npm:^1.2.0"
@@ -9435,6 +9436,28 @@ __metadata:
languageName: node
linkType: hard
+"framer-motion@npm:^12.11.0":
+ version: 12.11.0
+ resolution: "framer-motion@npm:12.11.0"
+ dependencies:
+ motion-dom: "npm:^12.11.0"
+ motion-utils: "npm:^12.9.4"
+ tslib: "npm:^2.4.0"
+ peerDependencies:
+ "@emotion/is-prop-valid": "*"
+ react: ^18.0.0 || ^19.0.0
+ react-dom: ^18.0.0 || ^19.0.0
+ peerDependenciesMeta:
+ "@emotion/is-prop-valid":
+ optional: true
+ react:
+ optional: true
+ react-dom:
+ optional: true
+ checksum: 10c0/369026997d12e51ba5ca12ecf507ccf2108941c9e634d0d57ff93524d56d72dc38cd909ce1272a155d15b92faf0ddf9268942908be29032e36cc535a353fd497
+ languageName: node
+ linkType: hard
+
"fresh@npm:^2.0.0":
version: 2.0.0
resolution: "fresh@npm:2.0.0"
@@ -13409,6 +13432,43 @@ __metadata:
languageName: node
linkType: hard
+"motion-dom@npm:^12.11.0":
+ version: 12.11.0
+ resolution: "motion-dom@npm:12.11.0"
+ dependencies:
+ motion-utils: "npm:^12.9.4"
+ checksum: 10c0/9fd7441c38b28560ea2db0f4dbd6f412873e777f5d32e623792cc8ff32c0bbff761f68102115060af81325227cc639e548f6123bdced50722f55bc2abda76b55
+ languageName: node
+ linkType: hard
+
+"motion-utils@npm:^12.9.4":
+ version: 12.9.4
+ resolution: "motion-utils@npm:12.9.4"
+ checksum: 10c0/b6783babfd1282ad320585f7cdac9fe7a1f97b39e07d12a500d3709534441bd9d49b556fa1cd838d1bde188570d4ab6b4c5aa9d297f7f5aa9dc16d600c17afdc
+ languageName: node
+ linkType: hard
+
+"motion@npm:^12.11.0":
+ version: 12.11.0
+ resolution: "motion@npm:12.11.0"
+ dependencies:
+ framer-motion: "npm:^12.11.0"
+ tslib: "npm:^2.4.0"
+ peerDependencies:
+ "@emotion/is-prop-valid": "*"
+ react: ^18.0.0 || ^19.0.0
+ react-dom: ^18.0.0 || ^19.0.0
+ peerDependenciesMeta:
+ "@emotion/is-prop-valid":
+ optional: true
+ react:
+ optional: true
+ react-dom:
+ optional: true
+ checksum: 10c0/1066de76a5d8a7a6898d8b47afe4363098466c67ed79cb989c0f05c11aa62a2cc83f61cc283ec07b452ae0622f6cd1c4584f1a0a68174798d67ba8811c52ac11
+ languageName: node
+ linkType: hard
+
"mri@npm:1.1.4":
version: 1.1.4
resolution: "mri@npm:1.1.4"