mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-01 09:49:03 +08:00
feat: enhance store page with new components and functionality
- Updated component paths in components.json for better organization. - Added 'motion' library to package.json for animations. - Refactored TypeScript configuration to include new renderer paths. - Implemented new StoreContent and StoreSidebar components for improved store layout. - Integrated store categories and items with filtering capabilities. - Enhanced UI with Tailwind CSS animations and styles for a better user experience.
This commit is contained in:
parent
0a0956cfc4
commit
184713dba8
@ -11,7 +11,7 @@
|
|||||||
"prefix": ""
|
"prefix": ""
|
||||||
},
|
},
|
||||||
"aliases": {
|
"aliases": {
|
||||||
"components": "@renderer",
|
"components": "@renderer/ui/third-party",
|
||||||
"utils": "@renderer/utils",
|
"utils": "@renderer/utils",
|
||||||
"ui": "@renderer/ui",
|
"ui": "@renderer/ui",
|
||||||
"lib": "@renderer/lib",
|
"lib": "@renderer/lib",
|
||||||
|
|||||||
@ -188,6 +188,7 @@
|
|||||||
"lru-cache": "^11.1.0",
|
"lru-cache": "^11.1.0",
|
||||||
"lucide-react": "^0.509.0",
|
"lucide-react": "^0.509.0",
|
||||||
"mime": "^4.0.4",
|
"mime": "^4.0.4",
|
||||||
|
"motion": "^12.11.0",
|
||||||
"npx-scope-finder": "^1.2.0",
|
"npx-scope-finder": "^1.2.0",
|
||||||
"openai": "patch:openai@npm%3A4.96.0#~/.yarn/patches/openai-npm-4.96.0-0665b05cb9.patch",
|
"openai": "patch:openai@npm%3A4.96.0#~/.yarn/patches/openai-npm-4.96.0-0665b05cb9.patch",
|
||||||
"p-queue": "^8.1.0",
|
"p-queue": "^8.1.0",
|
||||||
|
|||||||
@ -136,7 +136,7 @@ body[theme-mode='light'] {
|
|||||||
*::before,
|
*::before,
|
||||||
*::after {
|
*::after {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
margin: 0;
|
// margin: 0;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -109,6 +109,24 @@
|
|||||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||||
--color-sidebar-border: var(--sidebar-border);
|
--color-sidebar-border: var(--sidebar-border);
|
||||||
--color-sidebar-ring: var(--sidebar-ring);
|
--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 {
|
@layer base {
|
||||||
|
|||||||
@ -11,8 +11,8 @@ import { isEmoji } from '@renderer/utils'
|
|||||||
import type { MenuProps } from 'antd'
|
import type { MenuProps } from 'antd'
|
||||||
import { Avatar, Dropdown, Tooltip } from 'antd'
|
import { Avatar, Dropdown, Tooltip } from 'antd'
|
||||||
import {
|
import {
|
||||||
Box,
|
|
||||||
CircleHelp,
|
CircleHelp,
|
||||||
|
Compass,
|
||||||
FileSearch,
|
FileSearch,
|
||||||
Folder,
|
Folder,
|
||||||
Languages,
|
Languages,
|
||||||
@ -145,7 +145,7 @@ const MainMenus: FC = () => {
|
|||||||
minapp: <LayoutGrid size={18} className="icon" />,
|
minapp: <LayoutGrid size={18} className="icon" />,
|
||||||
knowledge: <FileSearch size={18} className="icon" />,
|
knowledge: <FileSearch size={18} className="icon" />,
|
||||||
files: <Folder size={17} className="icon" />,
|
files: <Folder size={17} className="icon" />,
|
||||||
store: <Box size={18} className="icon" />
|
store: <Compass size={18} className="icon" />
|
||||||
}
|
}
|
||||||
|
|
||||||
const pathMap = {
|
const pathMap = {
|
||||||
|
|||||||
207
src/renderer/src/pages/store/components/StoreContent.tsx
Normal file
207
src/renderer/src/pages/store/components/StoreContent.tsx
Normal file
@ -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 (
|
||||||
|
<div className="flex-1 overflow-auto">
|
||||||
|
{/* Sticky Header for Search, Filter, View Mode, and Category Tabs */}
|
||||||
|
<div className="sticky top-0 z-10 border-b bg-background p-4">
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
{/* Top row: Search, Filter, View buttons */}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="relative flex-1">
|
||||||
|
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
type="search"
|
||||||
|
placeholder="Search store..."
|
||||||
|
className="pl-8"
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => onSearchQueryChange(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="outline" size="icon">
|
||||||
|
<Filter className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
{/* Add actual filtering logic later */}
|
||||||
|
<DropdownMenuItem>Most Popular</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem>Newest</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem>Highest Rated</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => onViewModeChange('grid')}
|
||||||
|
className={cn(viewMode === 'grid' && 'bg-accent text-accent-foreground')}>
|
||||||
|
<Grid3X3 className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => onViewModeChange('list')}
|
||||||
|
className={cn(viewMode === 'list' && 'bg-accent text-accent-foreground')}>
|
||||||
|
<List className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Bottom row: Category Tabs */}
|
||||||
|
{/* <Tabs value={selectedCategory} onValueChange={onCategoryChange}>
|
||||||
|
<TabsList>
|
||||||
|
<TabsTrigger value="all">All</TabsTrigger>
|
||||||
|
<TabsTrigger value="mcp">MCP Services</TabsTrigger>
|
||||||
|
<TabsTrigger value="plugins">Plugins</TabsTrigger>
|
||||||
|
<TabsTrigger value="apps">Applications</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
</Tabs> */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Content Area: Grid or List View */}
|
||||||
|
<div className="p-4">
|
||||||
|
{items.length === 0 ? (
|
||||||
|
<div className="flex h-[400px] items-center justify-center">
|
||||||
|
<p className="text-center text-muted-foreground">No items found matching your criteria.</p>
|
||||||
|
</div>
|
||||||
|
) : viewMode === 'grid' ? (
|
||||||
|
<GridView items={items} />
|
||||||
|
) : (
|
||||||
|
<ListView items={items} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grid View Component
|
||||||
|
function GridView({ items }: { items: StoreItem[] }) {
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
|
||||||
|
{items.map((item, idx) => (
|
||||||
|
<BlurFade key={item.id} delay={0.25 + idx * 0.05} inView>
|
||||||
|
<Card key={item.id} className="overflow-hidden p-0">
|
||||||
|
<CardHeader className="p-0">
|
||||||
|
<div className="aspect-square w-full overflow-hidden bg-muted">
|
||||||
|
<img
|
||||||
|
src={item.image || '/placeholder.svg'} // Use placeholder if image missing
|
||||||
|
alt={item.title}
|
||||||
|
className="h-full w-full object-cover transition-transform hover:scale-105"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div>
|
||||||
|
<CardTitle className="line-clamp-1 text-base">{item.title}</CardTitle>
|
||||||
|
<p className="text-sm text-muted-foreground">{item.author}</p>
|
||||||
|
</div>
|
||||||
|
<Badge variant="outline">{item.type}</Badge>
|
||||||
|
</div>
|
||||||
|
<p className="mt-2 line-clamp-2 text-sm text-muted-foreground">{item.description}</p>
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter className="flex items-center justify-between p-4 pt-0">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Star className="h-3.5 w-3.5 fill-primary text-primary" />
|
||||||
|
<span className="text-sm">{item.rating}</span>
|
||||||
|
</div>
|
||||||
|
<Button size="sm">
|
||||||
|
<Download className="mr-2 h-3.5 w-3.5 text-white dark:text-black" />
|
||||||
|
Install
|
||||||
|
</Button>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
</BlurFade>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// List View Component
|
||||||
|
function ListView({ items }: { items: StoreItem[] }) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{items.map((item) => (
|
||||||
|
<Card key={item.id} className="p-0">
|
||||||
|
<div className="flex flex-col sm:flex-row">
|
||||||
|
<div className="h-24 w-24 shrink-0 overflow-hidden rounded-l-lg bg-muted sm:h-auto">
|
||||||
|
<img
|
||||||
|
src={item.image || '/placeholder.svg'} // Use placeholder if image missing
|
||||||
|
alt={item.title}
|
||||||
|
className="h-full w-full object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-1 flex-col justify-between p-4">
|
||||||
|
<div>
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div>
|
||||||
|
<h3 className="font-semibold">{item.title}</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">{item.author}</p>
|
||||||
|
</div>
|
||||||
|
<Badge variant="outline">{item.type}</Badge>
|
||||||
|
</div>
|
||||||
|
<p className="mt-2 line-clamp-2 text-sm text-muted-foreground">{item.description}</p>
|
||||||
|
<div className="mt-2 flex flex-wrap gap-1">
|
||||||
|
{item.tags.map((tag) => (
|
||||||
|
<Badge key={tag} variant="secondary" className="text-xs">
|
||||||
|
{tag}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between border-t p-4 sm:w-48 sm:flex-col sm:items-end sm:justify-center sm:border-l sm:border-t-0 sm:p-4">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Star className="h-3.5 w-3.5 fill-primary text-primary" />
|
||||||
|
<span className="text-sm">{item.rating}</span>
|
||||||
|
<span className="text-xs text-muted-foreground">({item.downloads})</span>
|
||||||
|
</div>
|
||||||
|
<Button size="sm" className="mt-2">
|
||||||
|
<Download className="mr-2 h-3.5 w-3.5 text-white dark:text-black" />
|
||||||
|
Install
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
57
src/renderer/src/pages/store/components/StoreSidebar.tsx
Normal file
57
src/renderer/src/pages/store/components/StoreSidebar.tsx
Normal file
@ -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 (
|
||||||
|
<Sidebar className="absolute left-0 top-0 h-full border-r">
|
||||||
|
<SidebarContent>
|
||||||
|
{categories.map((category) => (
|
||||||
|
<SidebarGroup key={category.title}>
|
||||||
|
{/* Only render label if it's not the 'all' category wrapper */}
|
||||||
|
{category.id !== 'all' && <SidebarGroupLabel>{category.title}</SidebarGroupLabel>}
|
||||||
|
<SidebarGroupContent>
|
||||||
|
<SidebarMenu>
|
||||||
|
{category.items.map((item) => (
|
||||||
|
<SidebarMenuItem key={item.id}>
|
||||||
|
<SidebarMenuButton
|
||||||
|
isActive={category.id === selectedCategory && item.id === selectedSubcategory}
|
||||||
|
className="justify-between"
|
||||||
|
onClick={() => {
|
||||||
|
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}
|
||||||
|
<Badge variant="secondary" className="ml-auto">
|
||||||
|
{item.count}
|
||||||
|
</Badge>
|
||||||
|
</SidebarMenuButton>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
))}
|
||||||
|
</SidebarMenu>
|
||||||
|
</SidebarGroupContent>
|
||||||
|
</SidebarGroup>
|
||||||
|
))}
|
||||||
|
</SidebarContent>
|
||||||
|
</Sidebar>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -1,72 +1,11 @@
|
|||||||
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
|
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
|
||||||
import { Badge } from '@renderer/ui/badge'
|
import { SidebarProvider } from '@renderer/ui/sidebar'
|
||||||
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 { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import categories from '../../../../../resources/data/store_categories.json'
|
import { StoreContent } from './components/StoreContent'
|
||||||
import storeList from '../../../../../resources/data/store_list.json'
|
import { StoreSidebar } from './components/StoreSidebar'
|
||||||
|
import storeList from './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 }
|
|
||||||
// ]
|
|
||||||
// }
|
|
||||||
// ]
|
|
||||||
|
|
||||||
export default function StoreLayout() {
|
export default function StoreLayout() {
|
||||||
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid')
|
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid')
|
||||||
@ -74,9 +13,8 @@ export default function StoreLayout() {
|
|||||||
const [selectedCategory, setSelectedCategory] = useState<string>('all')
|
const [selectedCategory, setSelectedCategory] = useState<string>('all')
|
||||||
const [selectedSubcategory, setSelectedSubcategory] = useState<string>('all')
|
const [selectedSubcategory, setSelectedSubcategory] = useState<string>('all')
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
// Update the filteredItems logic to use the new category IDs
|
|
||||||
const filteredItems = storeList.filter((item) => {
|
const filteredItems = storeList.filter((item) => {
|
||||||
// First apply search filter
|
|
||||||
const matchesSearch =
|
const matchesSearch =
|
||||||
searchQuery === '' ||
|
searchQuery === '' ||
|
||||||
item.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
item.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
@ -84,17 +22,38 @@ export default function StoreLayout() {
|
|||||||
item.author.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
item.author.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
item.tags.some((tag) => tag.toLowerCase().includes(searchQuery.toLowerCase()))
|
item.tags.some((tag) => tag.toLowerCase().includes(searchQuery.toLowerCase()))
|
||||||
|
|
||||||
// Then apply category filters
|
let matchesCategory = false
|
||||||
const matchesCategory =
|
if (selectedCategory === 'all') {
|
||||||
selectedCategory === 'all' ||
|
matchesCategory = true
|
||||||
item.categoryId === selectedCategory ||
|
} else if (['featured', 'new', 'top'].includes(selectedCategory)) {
|
||||||
(selectedCategory === 'featured' && item.featured)
|
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
|
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 (
|
return (
|
||||||
<div className="h-[calc(100vh_-_var(--navbar-height))] w-full">
|
<div className="h-[calc(100vh_-_var(--navbar-height))] w-full">
|
||||||
<Navbar className="h-full">
|
<Navbar className="h-full">
|
||||||
@ -102,190 +61,19 @@ export default function StoreLayout() {
|
|||||||
</Navbar>
|
</Navbar>
|
||||||
<div id="content-container" className="h-full w-full">
|
<div id="content-container" className="h-full w-full">
|
||||||
<SidebarProvider className="h-full w-full relative min-h-full">
|
<SidebarProvider className="h-full w-full relative min-h-full">
|
||||||
<Sidebar className="absolute left-0 top-0 h-full border-r">
|
<StoreSidebar
|
||||||
<SidebarHeader className="border-b px-4 py-4">
|
selectedCategory={selectedCategory}
|
||||||
<div className="flex items-center gap-2">
|
selectedSubcategory={selectedSubcategory}
|
||||||
<h2 className="text-xl font-semibold">Cherry Store</h2>
|
onSelectCategory={handleSelectCategory}
|
||||||
</div>
|
/>
|
||||||
</SidebarHeader>
|
<StoreContent
|
||||||
<SidebarContent>
|
viewMode={viewMode}
|
||||||
{categories.map((category) => (
|
searchQuery={searchQuery}
|
||||||
<SidebarGroup key={category.title}>
|
selectedCategory={selectedCategory}
|
||||||
<SidebarGroupLabel>{category.title}</SidebarGroupLabel>
|
items={filteredItems}
|
||||||
<SidebarGroupContent>
|
onSearchQueryChange={setSearchQuery}
|
||||||
<SidebarMenu>
|
onViewModeChange={setViewMode}
|
||||||
{category.items.map((item) => (
|
/>
|
||||||
<SidebarMenuItem key={item.id}>
|
|
||||||
<SidebarMenuButton
|
|
||||||
isActive={category.id === selectedCategory && item.id === selectedSubcategory}
|
|
||||||
className="justify-between"
|
|
||||||
onClick={() => {
|
|
||||||
setSelectedCategory(category.id)
|
|
||||||
setSelectedSubcategory(item.id)
|
|
||||||
}}>
|
|
||||||
{item.name}
|
|
||||||
<Badge variant="secondary" className="ml-auto">
|
|
||||||
{item.count}
|
|
||||||
</Badge>
|
|
||||||
</SidebarMenuButton>
|
|
||||||
</SidebarMenuItem>
|
|
||||||
))}
|
|
||||||
</SidebarMenu>
|
|
||||||
</SidebarGroupContent>
|
|
||||||
</SidebarGroup>
|
|
||||||
))}
|
|
||||||
</SidebarContent>
|
|
||||||
</Sidebar>
|
|
||||||
|
|
||||||
<div className="flex-1 overflow-auto">
|
|
||||||
<div className="sticky top-0 z-10 border-b bg-background p-4">
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div className="relative flex-1">
|
|
||||||
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
|
||||||
<Input
|
|
||||||
type="search"
|
|
||||||
placeholder="Search store..."
|
|
||||||
className="pl-8"
|
|
||||||
value={searchQuery}
|
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button variant="outline" size="icon">
|
|
||||||
<Filter className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align="end">
|
|
||||||
<DropdownMenuItem>Most Popular</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem>Newest</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem>Highest Rated</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="icon"
|
|
||||||
onClick={() => setViewMode('grid')}
|
|
||||||
className={cn(viewMode === 'grid' && 'bg-accent text-accent-foreground')}>
|
|
||||||
<Grid3X3 className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="icon"
|
|
||||||
onClick={() => setViewMode('list')}
|
|
||||||
className={cn(viewMode === 'list' && 'bg-accent text-accent-foreground')}>
|
|
||||||
<List className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Tabs
|
|
||||||
defaultValue="all"
|
|
||||||
value={selectedCategory}
|
|
||||||
onValueChange={(value) => {
|
|
||||||
setSelectedCategory(value)
|
|
||||||
setSelectedSubcategory('all')
|
|
||||||
}}>
|
|
||||||
<TabsList>
|
|
||||||
<TabsTrigger value="all">All</TabsTrigger>
|
|
||||||
<TabsTrigger value="mcp">MCP Services</TabsTrigger>
|
|
||||||
<TabsTrigger value="plugins">Plugins</TabsTrigger>
|
|
||||||
<TabsTrigger value="apps">Applications</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
</Tabs>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="p-4">
|
|
||||||
{filteredItems.length === 0 ? (
|
|
||||||
<div className="flex h-[400px] items-center justify-center">
|
|
||||||
<p className="text-center text-muted-foreground">No items found matching your search.</p>
|
|
||||||
</div>
|
|
||||||
) : viewMode === 'grid' ? (
|
|
||||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
|
|
||||||
{filteredItems.map((item) => (
|
|
||||||
<Card key={item.id} className="overflow-hidden p-0">
|
|
||||||
<CardHeader className="p-0">
|
|
||||||
<div className="aspect-square w-full overflow-hidden bg-muted">
|
|
||||||
<img
|
|
||||||
src={item.image || '/placeholder.svg'}
|
|
||||||
alt={item.title}
|
|
||||||
className="h-full w-full object-cover transition-transform hover:scale-105"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="p-4">
|
|
||||||
<div className="flex items-start justify-between">
|
|
||||||
<div>
|
|
||||||
<CardTitle className="line-clamp-1 text-base">{item.title}</CardTitle>
|
|
||||||
<p className="text-sm text-muted-foreground">{item.author}</p>
|
|
||||||
</div>
|
|
||||||
<Badge variant="outline">{item.type}</Badge>
|
|
||||||
</div>
|
|
||||||
<p className="mt-2 line-clamp-2 text-sm text-muted-foreground">{item.description}</p>
|
|
||||||
</CardContent>
|
|
||||||
<CardFooter className="flex items-center justify-between p-4 pt-0">
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<Star className="h-3.5 w-3.5 fill-primary text-primary" />
|
|
||||||
<span className="text-sm">{item.rating}</span>
|
|
||||||
</div>
|
|
||||||
<Button size="sm">
|
|
||||||
<Download className="mr-2 h-3.5 w-3.5 text-white! dark:text-black!" />
|
|
||||||
Install
|
|
||||||
</Button>
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="space-y-4">
|
|
||||||
{filteredItems.map((item) => (
|
|
||||||
<Card key={item.id} className="p-0">
|
|
||||||
<div className="flex flex-col sm:flex-row">
|
|
||||||
<div className="h-24 w-24 shrink-0 overflow-hidden rounded-l-lg bg-muted sm:h-auto">
|
|
||||||
<img
|
|
||||||
src={item.image || '/placeholder.svg'}
|
|
||||||
alt={item.title}
|
|
||||||
className="h-full w-full object-cover"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-1 flex-col justify-between p-4">
|
|
||||||
<div>
|
|
||||||
<div className="flex items-start justify-between">
|
|
||||||
<div>
|
|
||||||
<h3 className="font-semibold">{item.title}</h3>
|
|
||||||
<p className="text-sm text-muted-foreground">{item.author}</p>
|
|
||||||
</div>
|
|
||||||
<Badge variant="outline">{item.type}</Badge>
|
|
||||||
</div>
|
|
||||||
<p className="mt-2 line-clamp-2 text-sm text-muted-foreground">{item.description}</p>
|
|
||||||
<div className="mt-2 flex flex-wrap gap-1">
|
|
||||||
{item.tags.map((tag) => (
|
|
||||||
<Badge key={tag} variant="secondary" className="text-xs">
|
|
||||||
{tag}
|
|
||||||
</Badge>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between border-t p-4 sm:w-48 sm:flex-col sm:items-end sm:justify-center sm:border-l sm:border-t-0 sm:p-4">
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<Star className="h-3.5 w-3.5 fill-primary text-primary" />
|
|
||||||
<span className="text-sm">{item.rating}</span>
|
|
||||||
<span className="text-xs text-muted-foreground">({item.downloads})</span>
|
|
||||||
</div>
|
|
||||||
<Button size="sm" className="mt-2">
|
|
||||||
<Download className="mr-2 h-3.5 w-3.5 text-white! dark:text-black!" />
|
|
||||||
Install
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</SidebarProvider>
|
</SidebarProvider>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
66
src/renderer/src/ui/alert.tsx
Normal file
66
src/renderer/src/ui/alert.tsx
Normal file
@ -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<typeof alertVariants>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="alert"
|
||||||
|
role="alert"
|
||||||
|
className={cn(alertVariants({ variant }), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="alert-title"
|
||||||
|
className={cn(
|
||||||
|
"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertDescription({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="alert-description"
|
||||||
|
className={cn(
|
||||||
|
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Alert, AlertTitle, AlertDescription }
|
||||||
73
src/renderer/src/ui/third-party/BlurFade.tsx
vendored
Normal file
73
src/renderer/src/ui/third-party/BlurFade.tsx
vendored
Normal file
@ -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 (
|
||||||
|
<AnimatePresence>
|
||||||
|
<motion.div
|
||||||
|
ref={ref}
|
||||||
|
initial="hidden"
|
||||||
|
animate={isInView ? 'visible' : 'hidden'}
|
||||||
|
exit="hidden"
|
||||||
|
variants={combinedVariants}
|
||||||
|
transition={{
|
||||||
|
delay: 0.04 + delay,
|
||||||
|
duration,
|
||||||
|
ease: 'easeOut'
|
||||||
|
}}
|
||||||
|
className={className}
|
||||||
|
{...props}>
|
||||||
|
{children}
|
||||||
|
</motion.div>
|
||||||
|
</AnimatePresence>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -18,8 +18,8 @@
|
|||||||
"paths": {
|
"paths": {
|
||||||
"@main/*": ["src/main/*"],
|
"@main/*": ["src/main/*"],
|
||||||
"@types": ["src/renderer/src/types/index.ts"],
|
"@types": ["src/renderer/src/types/index.ts"],
|
||||||
"@components/*": ["packages/components/*"],
|
"@shared/*": ["packages/shared/*"],
|
||||||
"@shared/*": ["packages/shared/*"]
|
"@renderer/*": ["src/renderer/src/*"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
60
yarn.lock
60
yarn.lock
@ -5311,6 +5311,7 @@ __metadata:
|
|||||||
lucide-react: "npm:^0.509.0"
|
lucide-react: "npm:^0.509.0"
|
||||||
markdown-it: "npm:^14.1.0"
|
markdown-it: "npm:^14.1.0"
|
||||||
mime: "npm:^4.0.4"
|
mime: "npm:^4.0.4"
|
||||||
|
motion: "npm:^12.11.0"
|
||||||
next-themes: "npm:^0.4.6"
|
next-themes: "npm:^0.4.6"
|
||||||
node-stream-zip: "npm:^1.15.0"
|
node-stream-zip: "npm:^1.15.0"
|
||||||
npx-scope-finder: "npm:^1.2.0"
|
npx-scope-finder: "npm:^1.2.0"
|
||||||
@ -9435,6 +9436,28 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"fresh@npm:^2.0.0":
|
||||||
version: 2.0.0
|
version: 2.0.0
|
||||||
resolution: "fresh@npm:2.0.0"
|
resolution: "fresh@npm:2.0.0"
|
||||||
@ -13409,6 +13432,43 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"mri@npm:1.1.4":
|
||||||
version: 1.1.4
|
version: 1.1.4
|
||||||
resolution: "mri@npm:1.1.4"
|
resolution: "mri@npm:1.1.4"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user