mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-26 03:31:24 +08:00
feat: implement app state management and routing system
- Added AppStateService for managing application state in the database, including methods for getting, setting, and deleting state. - Introduced new API endpoint for app state storage with GET and PUT methods. - Integrated TanStack Router for routing, enabling a tabbed interface with dynamic routing and state synchronization. - Created AppShell component to manage layout and routing, including a sidebar and tab management. - Developed useTabs hook for handling tab state, including adding, closing, and switching tabs. - Updated package.json and yarn.lock to include necessary dependencies for routing and state management. This commit enhances the application's architecture by providing a robust state management system and a flexible routing mechanism, improving user experience and maintainability.
This commit is contained in:
parent
cf7b4dd07b
commit
aae39e3365
@ -1,3 +1,4 @@
|
||||
import { tanstackRouter } from '@tanstack/router-plugin/vite'
|
||||
import react from '@vitejs/plugin-react-swc'
|
||||
import { CodeInspectorPlugin } from 'code-inspector-plugin'
|
||||
import { defineConfig, externalizeDepsPlugin } from 'electron-vite'
|
||||
@ -80,6 +81,12 @@ export default defineConfig({
|
||||
},
|
||||
renderer: {
|
||||
plugins: [
|
||||
tanstackRouter({
|
||||
target: 'react',
|
||||
autoCodeSplitting: true,
|
||||
routesDirectory: './src/renderer/src/routes',
|
||||
generatedRouteTree: './src/renderer/src/routeTree.gen.ts'
|
||||
}),
|
||||
(async () => (await import('@tailwindcss/vite')).default())(),
|
||||
react({
|
||||
tsDecorators: true,
|
||||
|
||||
@ -182,7 +182,9 @@
|
||||
"@swc/plugin-styled-components": "^8.0.4",
|
||||
"@tailwindcss/vite": "^4.1.13",
|
||||
"@tanstack/react-query": "^5.85.5",
|
||||
"@tanstack/react-router": "^1.139.3",
|
||||
"@tanstack/react-virtual": "^3.13.12",
|
||||
"@tanstack/router-plugin": "^1.139.3",
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
@ -220,8 +222,8 @@
|
||||
"@types/mime-types": "^3",
|
||||
"@types/node": "^22.17.1",
|
||||
"@types/pako": "^1.0.2",
|
||||
"@types/react": "^19.0.12",
|
||||
"@types/react-dom": "^19.0.4",
|
||||
"@types/react": "^19.2.7",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@types/react-infinite-scroll-component": "^5.0.0",
|
||||
"@types/react-transition-group": "^4.4.12",
|
||||
"@types/react-window": "^1",
|
||||
|
||||
@ -18,6 +18,25 @@ export type { ConcreteApiPaths } from './apiPaths'
|
||||
* enabling full TypeScript type checking across IPC boundaries.
|
||||
*/
|
||||
export interface ApiSchemas {
|
||||
/**
|
||||
* App State storage endpoint
|
||||
* @example GET /app/state/tabs_state
|
||||
* @example PUT /app/state/tabs_state { "value": {...} }
|
||||
*/
|
||||
'/app/state/:key': {
|
||||
/** Get app state by key */
|
||||
GET: {
|
||||
params: { key: string }
|
||||
response: any
|
||||
}
|
||||
/** Save app state */
|
||||
PUT: {
|
||||
params: { key: string }
|
||||
body: any
|
||||
response: { success: boolean }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test items collection endpoint
|
||||
* @example GET /test/items?page=1&limit=10&search=hello
|
||||
|
||||
8
packages/shared/data/cache/cacheSchemas.ts
vendored
8
packages/shared/data/cache/cacheSchemas.ts
vendored
@ -26,6 +26,9 @@ export type UseCacheSchema = {
|
||||
'topic.active': CacheValueTypes.CacheTopic | null
|
||||
'topic.renaming': string[]
|
||||
'topic.newly_renamed': string[]
|
||||
|
||||
// UI State
|
||||
'ui.activeTabId': string
|
||||
}
|
||||
|
||||
export const DefaultUseCache: UseCacheSchema = {
|
||||
@ -56,7 +59,10 @@ export const DefaultUseCache: UseCacheSchema = {
|
||||
// Topic management
|
||||
'topic.active': null,
|
||||
'topic.renaming': [],
|
||||
'topic.newly_renamed': []
|
||||
'topic.newly_renamed': [],
|
||||
|
||||
// UI State
|
||||
'ui.activeTabId': ''
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
* TypeScript will error if any endpoint is missing.
|
||||
*/
|
||||
|
||||
import { appStateService } from '@data/services/AppStateService'
|
||||
import { TestService } from '@data/services/TestService'
|
||||
import type { ApiImplementation } from '@shared/data/api/apiSchemas'
|
||||
|
||||
@ -16,6 +17,18 @@ const testService = TestService.getInstance()
|
||||
* Must implement every path+method combination from ApiSchemas
|
||||
*/
|
||||
export const apiHandlers: ApiImplementation = {
|
||||
'/app/state/:key': {
|
||||
GET: async ({ params }) => {
|
||||
const state = await appStateService.getState(params.key)
|
||||
return state
|
||||
},
|
||||
|
||||
PUT: async ({ params, body }) => {
|
||||
await appStateService.setState(params.key, body)
|
||||
return { success: true }
|
||||
}
|
||||
},
|
||||
|
||||
'/test/items': {
|
||||
GET: async ({ query }) => {
|
||||
return await testService.getItems({
|
||||
|
||||
100
src/main/data/services/AppStateService.ts
Normal file
100
src/main/data/services/AppStateService.ts
Normal file
@ -0,0 +1,100 @@
|
||||
import { dbService } from '@data/db/DbService'
|
||||
import { appStateTable } from '@data/db/schemas/appState'
|
||||
import { loggerService } from '@logger'
|
||||
import { eq } from 'drizzle-orm'
|
||||
|
||||
const logger = loggerService.withContext('AppStateService')
|
||||
|
||||
/**
|
||||
* Service for managing application state in the database.
|
||||
* Provides key-value storage for persisting UI state like tabs, window positions, etc.
|
||||
*/
|
||||
export class AppStateService {
|
||||
private static instance: AppStateService
|
||||
|
||||
private constructor() {}
|
||||
|
||||
public static getInstance(): AppStateService {
|
||||
if (!AppStateService.instance) {
|
||||
AppStateService.instance = new AppStateService()
|
||||
}
|
||||
return AppStateService.instance
|
||||
}
|
||||
|
||||
/**
|
||||
* Get app state by key
|
||||
* @param key - The state key to retrieve
|
||||
* @returns The stored value or null if not found
|
||||
*/
|
||||
async getState<T = unknown>(key: string): Promise<T | null> {
|
||||
try {
|
||||
const db = dbService.getDb()
|
||||
const result = await db.select().from(appStateTable).where(eq(appStateTable.key, key)).limit(1)
|
||||
|
||||
if (result.length === 0) {
|
||||
logger.debug('App state not found', { key })
|
||||
return null
|
||||
}
|
||||
|
||||
logger.debug('Retrieved app state', { key })
|
||||
return result[0].value as T
|
||||
} catch (error) {
|
||||
logger.error('Failed to get app state', error as Error, { key })
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save app state by key (upsert)
|
||||
* @param key - The state key
|
||||
* @param value - The value to store (will be JSON serialized)
|
||||
* @param description - Optional description of what this state stores
|
||||
*/
|
||||
async setState<T = unknown>(key: string, value: T, description?: string): Promise<void> {
|
||||
try {
|
||||
const db = dbService.getDb()
|
||||
|
||||
await db
|
||||
.insert(appStateTable)
|
||||
.values({
|
||||
key,
|
||||
value: value as any,
|
||||
description
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: appStateTable.key,
|
||||
set: {
|
||||
value: value as any,
|
||||
description,
|
||||
updatedAt: Date.now()
|
||||
}
|
||||
})
|
||||
|
||||
logger.debug('Saved app state', { key })
|
||||
} catch (error) {
|
||||
logger.error('Failed to save app state', error as Error, { key })
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete app state by key
|
||||
* @param key - The state key to delete
|
||||
* @returns true if deleted, false if not found
|
||||
*/
|
||||
async deleteState(key: string): Promise<boolean> {
|
||||
try {
|
||||
const db = dbService.getDb()
|
||||
const result = await db.delete(appStateTable).where(eq(appStateTable.key, key))
|
||||
|
||||
const deleted = result.rowsAffected > 0
|
||||
logger.debug('Deleted app state', { key, deleted })
|
||||
return deleted
|
||||
} catch (error) {
|
||||
logger.error('Failed to delete app state', error as Error, { key })
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const appStateService = AppStateService.getInstance()
|
||||
17
src/renderer/src/AppRouter.tsx
Normal file
17
src/renderer/src/AppRouter.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import { createRouter, RouterProvider } from '@tanstack/react-router'
|
||||
|
||||
import { routeTree } from './routeTree.gen'
|
||||
|
||||
// Create a new router instance
|
||||
const router = createRouter({ routeTree })
|
||||
|
||||
// Register the router instance for type safety
|
||||
declare module '@tanstack/react-router' {
|
||||
interface Register {
|
||||
router: typeof router
|
||||
}
|
||||
}
|
||||
|
||||
export const AppRouter = () => {
|
||||
return <RouterProvider router={router} />
|
||||
}
|
||||
187
src/renderer/src/components/layout/AppShell.tsx
Normal file
187
src/renderer/src/components/layout/AppShell.tsx
Normal file
@ -0,0 +1,187 @@
|
||||
// TODO demo component
|
||||
import { cn } from '@cherrystudio/ui'
|
||||
import { Link, Outlet, useLocation, useNavigate } from '@tanstack/react-router'
|
||||
import { X } from 'lucide-react'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
import { useTabs } from '../../hooks/useTabs'
|
||||
|
||||
// Mock Sidebar component (Replace with actual one later)
|
||||
const Sidebar = ({ onNavigate }: { onNavigate: (id: string) => void }) => {
|
||||
// Helper to render a Sidebar Link that acts as a Tab Switcher
|
||||
const SidebarItem = ({ to, title, id }: { to: string; title: string; id: string }) => (
|
||||
<Link
|
||||
to={to}
|
||||
className="flex h-10 w-10 items-center justify-center rounded-md hover:bg-accent data-[status=active]:bg-primary/20 data-[status=active]:font-bold"
|
||||
activeProps={{
|
||||
'data-status': 'active'
|
||||
}}
|
||||
onClick={(e) => {
|
||||
// Intercept the router navigation!
|
||||
// We want to switch tabs, not just navigate within the current tab.
|
||||
e.preventDefault()
|
||||
onNavigate(id)
|
||||
}}>
|
||||
{title.slice(0, 1).toUpperCase() + title.slice(1, 3)}
|
||||
</Link>
|
||||
)
|
||||
|
||||
return (
|
||||
<aside className="flex h-full w-16 flex-col items-center gap-4 border-r bg-muted/10 py-4">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-md bg-primary/20 font-bold text-xs">Logo</div>
|
||||
|
||||
<SidebarItem to="/" title="Home" id="home" />
|
||||
<SidebarItem to="/settings" title="Settings" id="settings" />
|
||||
|
||||
<div className="flex-1" />
|
||||
<button type="button" className="flex h-10 w-10 items-center justify-center rounded-md hover:bg-accent">
|
||||
User
|
||||
</button>
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
|
||||
// Mock MinApp component (Replace with actual implementation)
|
||||
const MinApp = ({ url }: { url: string }) => (
|
||||
<div className="flex h-full w-full flex-col items-center justify-center bg-background">
|
||||
<div className="mb-2 font-bold text-lg">Webview App</div>
|
||||
<code className="rounded bg-muted p-2">{url}</code>
|
||||
</div>
|
||||
)
|
||||
|
||||
export const AppShell = () => {
|
||||
const { tabs, activeTabId, setActiveTab, closeTab, addTab, updateTab } = useTabs()
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
|
||||
// 1. Sync Route -> Tab (Handle internal navigation & deep links)
|
||||
useEffect(() => {
|
||||
const currentPath = location.pathname
|
||||
const activeTab = tabs.find((t) => t.id === activeTabId)
|
||||
|
||||
if (activeTab?.type === 'url' && activeTab.url !== currentPath) {
|
||||
const existingTab = tabs.find((t) => t.type === 'url' && t.url === currentPath && t.id !== activeTabId)
|
||||
if (existingTab) {
|
||||
setActiveTab(existingTab.id)
|
||||
} else {
|
||||
// Sync URL changes back to DB
|
||||
updateTab(activeTabId, { url: currentPath })
|
||||
}
|
||||
}
|
||||
}, [location.pathname, tabs, activeTabId, setActiveTab, updateTab])
|
||||
|
||||
// 2. Sync Tab -> Route (Handle tab switching)
|
||||
useEffect(() => {
|
||||
const activeTab = tabs.find((t) => t.id === activeTabId)
|
||||
if (!activeTab) return
|
||||
|
||||
if (activeTab.type === 'url') {
|
||||
if (location.pathname !== activeTab.url) {
|
||||
navigate({ to: activeTab.url })
|
||||
}
|
||||
}
|
||||
}, [activeTabId, tabs, navigate, location.pathname])
|
||||
|
||||
const handleSidebarClick = (menuId: string) => {
|
||||
let targetUrl = ''
|
||||
let targetTitle = ''
|
||||
|
||||
switch (menuId) {
|
||||
case 'home':
|
||||
targetUrl = '/'
|
||||
targetTitle = 'Home'
|
||||
break
|
||||
case 'settings':
|
||||
targetUrl = '/settings'
|
||||
targetTitle = 'Settings'
|
||||
break
|
||||
default:
|
||||
return
|
||||
}
|
||||
|
||||
const existingTab = tabs.find((t) => t.type === 'url' && t.url === targetUrl)
|
||||
|
||||
if (existingTab) {
|
||||
setActiveTab(existingTab.id)
|
||||
} else {
|
||||
addTab({
|
||||
id: `${menuId}-${Date.now()}`,
|
||||
type: 'url',
|
||||
url: targetUrl,
|
||||
title: targetTitle
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const activeTab = tabs.find((t) => t.id === activeTabId)
|
||||
const isWebviewActive = activeTab?.type === 'webview'
|
||||
|
||||
return (
|
||||
<div className="flex h-screen w-screen flex-row overflow-hidden bg-background text-foreground">
|
||||
{/* Zone 1: Sidebar */}
|
||||
<Sidebar onNavigate={handleSidebarClick} />
|
||||
|
||||
<div className="flex h-full min-w-0 flex-1 flex-col">
|
||||
{/* Zone 2: Tab Bar */}
|
||||
<header className="flex h-10 w-full items-center border-b bg-muted/5">
|
||||
<div className="hide-scrollbar flex-1 overflow-x-auto">
|
||||
<div className="flex h-full w-full items-center justify-start">
|
||||
{tabs.map((tab) => (
|
||||
<Link
|
||||
key={tab.id}
|
||||
to={tab.url}
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
className={cn(
|
||||
'relative flex h-full min-w-[120px] max-w-[200px] items-center justify-between gap-2 border-border/40 border-r px-3 py-2 text-sm transition-colors hover:bg-muted/50',
|
||||
tab.id === activeTabId ? 'bg-background shadow-sm' : 'bg-transparent opacity-70 hover:opacity-100'
|
||||
)}>
|
||||
<span className="truncate text-xs">{tab.title}</span>
|
||||
<div
|
||||
role="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
closeTab(tab.id)
|
||||
}}
|
||||
className="ml-1 cursor-pointer rounded-sm p-0.5 opacity-50 hover:bg-muted-foreground/20 hover:opacity-100">
|
||||
<X className="size-3" />
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Zone 3: Content Area (Simplified Hybrid Architecture) */}
|
||||
<main className="relative flex-1 overflow-hidden bg-background">
|
||||
{/* Layer A: Standard Router Outlet */}
|
||||
{/* Always rendered, but hidden if a webview is active. This keeps the Router alive. */}
|
||||
<div
|
||||
style={{
|
||||
display: isWebviewActive ? 'none' : 'block',
|
||||
height: '100%',
|
||||
width: '100%'
|
||||
}}>
|
||||
<Outlet />
|
||||
</div>
|
||||
|
||||
{/* Layer B: Webview Apps (Overlay) */}
|
||||
{tabs.map((tab) => {
|
||||
if (tab.type !== 'webview') return null
|
||||
return (
|
||||
<div
|
||||
key={tab.id}
|
||||
style={{
|
||||
display: tab.id === activeTabId ? 'block' : 'none',
|
||||
height: '100%',
|
||||
width: '100%'
|
||||
}}>
|
||||
<MinApp url={tab.url} />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
107
src/renderer/src/components/layout/router-architecture.md
Normal file
107
src/renderer/src/components/layout/router-architecture.md
Normal file
@ -0,0 +1,107 @@
|
||||
# Router Architecture & Tab System
|
||||
|
||||
## Overview
|
||||
|
||||
This document describes the routing and tab management architecture for Cherry Studio. The system implements a "Chrome-like" tabbed interface where every tab can be a distinct application state (Chat, Settings, MinApp/Webview).
|
||||
|
||||
The architecture is **Hybrid**, combining:
|
||||
|
||||
1. **TanStack Router (HashRouter)**: Handles URL parsing and standard React page rendering. Standard pages (Home, Settings) are re-mounted on tab switch but optimized via data caching.
|
||||
2. **Webview Overlay System**: Manages persistent processes (Webviews) that live *outside* the router to ensure they are never destroyed during navigation.
|
||||
3. **Bidirectional State Sync**: `AppShell` ensures the URL bar and the Tab Database (`app_state` table) are always in sync.
|
||||
|
||||
## Core Architecture
|
||||
|
||||
### 1. Hybrid Rendering
|
||||
|
||||
We use a "Single Router + Overlay" approach. We do **not** force `KeepAlive` for standard React pages, as it complicates the router logic significantly. Instead, we rely on TanStack Router's fast caching to make re-mounting feel instant.
|
||||
|
||||
* **Layer 1: Standard Router (The Outlet)**
|
||||
* Always present in the DOM.
|
||||
* Renders standard pages (Home, Settings, Native Chat).
|
||||
* **Behavior**: When switching between standard tabs (e.g., Home -> Settings), the router navigates, unmounting the old component and mounting the new one.
|
||||
* **Optimization**: Data loaders are cached, so "re-mounting" is cheap and fast.
|
||||
* **Visibility**: Hidden via `display: none` if a Webview tab is active.
|
||||
|
||||
* **Layer 2: Webview Overlays**
|
||||
* Rendered **outside** the Router.
|
||||
* **Behavior**: These components are *never* unmounted as long as the tab is open.
|
||||
* **Visibility**: Controlled purely by CSS (`display: block` / `none`).
|
||||
* **Purpose**: To keep heavy processes (like MinApps or external websites) alive in the background.
|
||||
|
||||
### 2. State Synchronization (The "Listener" Pattern)
|
||||
|
||||
Since we use a single Router instance, we must manually sync the "Active Tab's URL" with the "Router's URL".
|
||||
|
||||
* **URL -> Database (Passive Sync)**:
|
||||
* A `useEffect` hook in `AppShell` listens to `location.pathname`.
|
||||
* If the URL changes (e.g., user navigates inside a Chat tab), we update the current tab's `url` field in the SQLite database.
|
||||
* *Benefit*: Restores the exact sub-route (e.g., `/chat/session-123`) when the user comes back later.
|
||||
|
||||
* **Tab Switch -> Router (Active Navigation)**:
|
||||
* When the user clicks a tab, we read its stored `url` from the database.
|
||||
* We calling `navigate({ to: storedUrl })` to restore the view.
|
||||
|
||||
### 3. Data Management
|
||||
|
||||
* **Storage**: Tab data (`tabs`, `activeTabId`) is stored in SQLite (`app_state` table).
|
||||
* **Sync**: `useTabs` hook uses SWR (`useQuery`) to sync frontend state with the database.
|
||||
* **Optimistic Updates**: UI updates immediately, background sync handles persistence.
|
||||
|
||||
## Routing & Overlay Mapping
|
||||
|
||||
For detailed route tree definitions and component mappings, please refer to [Router Planning](./router-planning.md).
|
||||
|
||||
### Handling Webview Routes
|
||||
|
||||
The planning document mentions routes like `/apps/$appId` that may correspond to Webview applications. In our Hybrid Architecture, these are handled as follows:
|
||||
|
||||
1. **Router Layer**: The route `/apps/$appId` is still defined in TSR.
|
||||
* Purpose: Maintains URL semantics and supports deep linking.
|
||||
* Rendering: Renders a "shell" component or loading state.
|
||||
2. **Overlay Layer**: `AppShell` detects that the current Tab type is `webview`.
|
||||
* Behavior: Hides the Router's Outlet.
|
||||
* Rendering: Displays the corresponding `<Webview />` instance in the Overlay layer.
|
||||
|
||||
This mechanism ensures that even Webview apps have standard URLs, providing a consistent navigation experience across the application.
|
||||
|
||||
## Key Components
|
||||
|
||||
### `AppShell` (`src/renderer/src/components/layout/AppShell.tsx`)
|
||||
|
||||
The coordinator that manages the two layers.
|
||||
|
||||
```tsx
|
||||
return (
|
||||
<div className="app-shell">
|
||||
<Sidebar />
|
||||
<div className="main-content">
|
||||
<TabBar />
|
||||
|
||||
{/* Layer 1: Standard Router (Hidden if Webview is active) */}
|
||||
<div style={{ display: isWebviewActive ? 'none' : 'block' }}>
|
||||
<Outlet />
|
||||
</div>
|
||||
|
||||
{/* Layer 2: Webview Overlays (Only for type='webview') */}
|
||||
{tabs.map(tab => {
|
||||
if (tab.type !== 'webview') return null;
|
||||
return (
|
||||
<div style={{ display: isActive ? 'block' : 'none' }}>
|
||||
<Webview url={tab.url} />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
```
|
||||
|
||||
## Trade-offs
|
||||
|
||||
| Feature | Approach | Rationale |
|
||||
| :--- | :--- | :--- |
|
||||
| **Standard Pages** | Re-mount on switch | Simplicity. Reactivity problems with KeepAlive are avoided. TSR caching makes it fast. |
|
||||
| **Webviews** | Keep-Alive (CSS Hide) | Essential. Reloading an external app/website is bad UX. |
|
||||
| **Routing** | HashRouter | Native to Electron file system. Avoids history API complexities. |
|
||||
| **URL Logic** | Single Source of Truth | The address bar always reflects the *active* tab. Background tabs are just state in DB. |
|
||||
347
src/renderer/src/components/layout/router-planning.md
Normal file
347
src/renderer/src/components/layout/router-planning.md
Normal file
@ -0,0 +1,347 @@
|
||||
# Router Planning
|
||||
|
||||
> Version: v0.1.0
|
||||
> Updated: 2025-11-25
|
||||
> Status: Draft
|
||||
|
||||
## 1. Overview
|
||||
|
||||
This document defines the routing structure plan for migrating Cherry Studio from React Router to TanStack Router (TSR).
|
||||
|
||||
### 1.1 Core Interaction Model
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Left Sidebar │ Top Tab Bar │
|
||||
│ (Shortcuts) │ [Tab1] [Tab2] [Tab3] [+] │
|
||||
│ ┌───────────┐ ├──────────────────────────────────────────┤
|
||||
│ │ 💬 Chat │ │ │
|
||||
│ │ ⚙️ Settings│ │ Content Area (Outlet) │
|
||||
│ │ 📁 Files │ │ │
|
||||
│ │ 📝 Notes │ │ Rendered based on active Tab's URL │
|
||||
│ │ ... │ │ │
|
||||
│ └───────────┘ │ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
- **Left Sidebar**: Like a "bookmarks bar", stores shortcuts. Clicking navigates to the URL (may reuse existing Tab or create new Tab)
|
||||
- **Top Tab Bar**: Manages multiple open pages, supports closing and switching
|
||||
- **Content Area**: Rendered by TanStack Router's `<Outlet />`
|
||||
|
||||
---
|
||||
|
||||
## 2. Route Structure
|
||||
|
||||
### 2.1 Directory Structure
|
||||
|
||||
```
|
||||
src/renderer/src/routes/
|
||||
├── __root.tsx # Root route → AppShell
|
||||
├── index.tsx # / → Welcome page or redirect (TBD)
|
||||
│
|
||||
├── chat/
|
||||
│ ├── route.tsx # /chat layout: sidebar + <Outlet/>
|
||||
│ ├── index.tsx # /chat → Empty state (no topic selected)
|
||||
│ └── $assistantId/
|
||||
│ ├── route.tsx # /chat/$assistantId layout (optional)
|
||||
│ ├── index.tsx # /chat/$assistantId → Assistant home (optional)
|
||||
│ └── $topicId.tsx # /chat/$assistantId/$topicId → Chat view
|
||||
│
|
||||
├── settings/
|
||||
│ ├── route.tsx # /settings layout: menu + <Outlet/>
|
||||
│ ├── index.tsx # /settings → Redirect to default sub-page
|
||||
│ ├── provider.tsx # /settings/provider
|
||||
│ ├── model.tsx # /settings/model
|
||||
│ ├── general.tsx # /settings/general
|
||||
│ ├── display.tsx # /settings/display
|
||||
│ ├── data.tsx # /settings/data
|
||||
│ ├── mcp.tsx # /settings/mcp
|
||||
│ ├── shortcut.tsx # /settings/shortcut
|
||||
│ └── about.tsx # /settings/about
|
||||
│
|
||||
├── knowledge/
|
||||
│ ├── route.tsx # /knowledge layout
|
||||
│ ├── index.tsx # /knowledge → Knowledge base list
|
||||
│ └── $baseId.tsx # /knowledge/$baseId → Knowledge base detail
|
||||
│
|
||||
├── notes/
|
||||
│ ├── route.tsx # /notes layout: tree sidebar + <Outlet/>
|
||||
│ ├── index.tsx # /notes → Empty state
|
||||
│ └── $noteId.tsx # /notes/$noteId → Editor
|
||||
│
|
||||
├── apps/
|
||||
│ ├── route.tsx # /apps layout
|
||||
│ ├── index.tsx # /apps → App list
|
||||
│ └── $appId.tsx # /apps/$appId → App detail (possibly Webview)
|
||||
│
|
||||
├── paintings/
|
||||
│ ├── route.tsx # /paintings layout: provider select + <Outlet/>
|
||||
│ ├── index.tsx # /paintings → Redirect to default provider
|
||||
│ ├── zhipu.tsx # /paintings/zhipu → Zhipu painting
|
||||
│ ├── aihubmix.tsx # /paintings/aihubmix → Aihubmix
|
||||
│ ├── silicon.tsx # /paintings/silicon → Silicon Flow
|
||||
│ ├── dmxapi.tsx # /paintings/dmxapi → Dmxapi
|
||||
│ ├── tokenflux.tsx # /paintings/tokenflux → TokenFlux
|
||||
│ ├── ovms.tsx # /paintings/ovms → OVMS
|
||||
│ └── $providerId.tsx # /paintings/$providerId → Dynamic NewApi provider
|
||||
│
|
||||
├── files.tsx # /files → File management
|
||||
├── translate.tsx # /translate → Translation
|
||||
├── store.tsx # /store → App store
|
||||
└── launchpad.tsx # /launchpad → Launchpad
|
||||
```
|
||||
|
||||
### 2.2 Route Table
|
||||
|
||||
| Route | Component | Loader Data | Description |
|
||||
|-------|-----------|-------------|-------------|
|
||||
| `/` | `WelcomePage` | - | Welcome page or redirect (TBD) |
|
||||
| `/chat` | `ChatLayout` | Assistants, Topics | Chat layout layer |
|
||||
| `/chat/$assistantId/$topicId` | `ChatView` | Topic detail, Messages | Chat main view |
|
||||
| `/settings` | `SettingsLayout` | - | Settings layout layer |
|
||||
| `/settings/provider` | `ProviderSettings` | Provider list | Provider settings |
|
||||
| `/settings/model` | `ModelSettings` | Model list | Model settings |
|
||||
| `/settings/*` | `*Settings` | Respective data | Other settings pages |
|
||||
| `/knowledge` | `KnowledgeLayout` | Knowledge bases | Knowledge layout |
|
||||
| `/knowledge/$baseId` | `KnowledgeDetail` | Knowledge detail | Knowledge detail page |
|
||||
| `/notes` | `NotesLayout` | Notes tree | Notes layout |
|
||||
| `/notes/$noteId` | `NotesEditor` | Note content | Notes editor |
|
||||
| `/apps` | `AppsLayout` | App list | Apps layout |
|
||||
| `/apps/$appId` | `AppDetail` | App detail | App detail/Webview |
|
||||
| `/paintings` | `PaintingsLayout` | Provider list | Paintings layout layer |
|
||||
| `/paintings/zhipu` | `ZhipuPage` | - | Zhipu painting |
|
||||
| `/paintings/aihubmix` | `AihubmixPage` | - | Aihubmix painting |
|
||||
| `/paintings/silicon` | `SiliconPage` | - | Silicon Flow painting |
|
||||
| `/paintings/dmxapi` | `DmxapiPage` | - | Dmxapi painting |
|
||||
| `/paintings/tokenflux` | `TokenFluxPage` | - | TokenFlux painting |
|
||||
| `/paintings/ovms` | `OvmsPage` | - | OVMS painting |
|
||||
| `/paintings/$providerId` | `NewApiPage` | - | Dynamic NewApi provider |
|
||||
| `/files` | `FilesPage` | File list | File management |
|
||||
| `/translate` | `TranslatePage` | - | Translation page |
|
||||
| `/store` | `StorePage` | Store data | App store |
|
||||
| `/launchpad` | `LaunchpadPage` | - | Launchpad |
|
||||
|
||||
---
|
||||
|
||||
## 3. Chat Route Design
|
||||
|
||||
### 3.1 URL Structure
|
||||
|
||||
```
|
||||
/chat/$assistantId/$topicId
|
||||
│ │
|
||||
│ └── Topic ID (conversation ID)
|
||||
└── Assistant ID
|
||||
```
|
||||
|
||||
**Examples**:
|
||||
|
||||
- `/chat` → Chat home (sidebar + empty state)
|
||||
- `/chat/assistant-1` → Assistant 1's home (optional, may redirect to first topic)
|
||||
- `/chat/assistant-1/topic-123` → Chat view for topic 123 under assistant 1
|
||||
|
||||
### 3.2 Component Structure
|
||||
|
||||
```tsx
|
||||
// routes/chat/route.tsx
|
||||
export const Route = createFileRoute('/chat')({
|
||||
component: ChatLayout,
|
||||
loader: async () => ({
|
||||
assistants: await fetchAssistants(),
|
||||
topics: await fetchTopics()
|
||||
}),
|
||||
staleTime: 30_000,
|
||||
})
|
||||
|
||||
function ChatLayout() {
|
||||
const data = Route.useLoaderData()
|
||||
|
||||
return (
|
||||
<div className="flex h-full">
|
||||
{/* Sidebar: Assistant list + Topic list */}
|
||||
<ChatSidebar assistants={data.assistants} topics={data.topics} />
|
||||
|
||||
{/* Chat content area */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
// routes/chat/$assistantId/$topicId.tsx
|
||||
export const Route = createFileRoute('/chat/$assistantId/$topicId')({
|
||||
component: ChatView,
|
||||
loader: async ({ params }) => ({
|
||||
topic: await fetchTopic(params.topicId),
|
||||
messages: await fetchMessages(params.topicId)
|
||||
}),
|
||||
staleTime: 10_000,
|
||||
})
|
||||
|
||||
function ChatView() {
|
||||
const { topic, messages } = Route.useLoaderData()
|
||||
const { assistantId, topicId } = Route.useParams()
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<ChatNavbar topic={topic} />
|
||||
<Messages messages={messages} />
|
||||
<Inputbar topicId={topicId} assistantId={assistantId} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 Data Flow
|
||||
|
||||
```
|
||||
1. User clicks topic in sidebar
|
||||
↓
|
||||
2. navigate({ to: '/chat/$assistantId/$topicId' })
|
||||
↓
|
||||
3. TSR matches route, checks loader cache
|
||||
↓
|
||||
4. Cache hit → Render directly
|
||||
Cache miss → Execute loader, fetch data
|
||||
↓
|
||||
5. ChatLayout does not re-render (parent route data cached)
|
||||
↓
|
||||
6. Only ChatView updates (child route data changed)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Settings Route Design
|
||||
|
||||
### 4.1 Sub-page List
|
||||
|
||||
| Route | Component | Existing File |
|
||||
|-------|-----------|---------------|
|
||||
| `/settings/provider` | `ProviderSettings` | `ProviderSettings/` |
|
||||
| `/settings/model` | `ModelSettings` | `ModelSettings/` |
|
||||
| `/settings/general` | `GeneralSettings` | `GeneralSettings.tsx` |
|
||||
| `/settings/display` | `DisplaySettings` | `DisplaySettings.tsx` |
|
||||
| `/settings/data` | `DataSettings` | `DataSettings/` |
|
||||
| `/settings/mcp` | `MCPSettings` | `MCPSettings/` |
|
||||
| `/settings/websearch` | `WebSearchSettings` | `WebSearchSettings/` |
|
||||
| `/settings/memory` | `MemorySettings` | `MemorySettings/` |
|
||||
| `/settings/shortcut` | `ShortcutSettings` | `ShortcutSettings.tsx` |
|
||||
| `/settings/quickassistant` | `QuickAssistantSettings` | `QuickAssistantSettings.tsx` |
|
||||
| `/settings/about` | `AboutSettings` | `AboutSettings.tsx` |
|
||||
|
||||
### 4.2 Layout Structure
|
||||
|
||||
```tsx
|
||||
// routes/settings/route.tsx
|
||||
function SettingsLayout() {
|
||||
return (
|
||||
<div className="flex h-full">
|
||||
{/* Left menu */}
|
||||
<SettingsMenu />
|
||||
|
||||
{/* Right content */}
|
||||
<div className="flex-1 overflow-auto">
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Paintings Route Design
|
||||
|
||||
### 5.1 URL Structure
|
||||
|
||||
```
|
||||
/paintings/$providerId
|
||||
│
|
||||
└── Provider ID (zhipu, aihubmix, silicon, dmxapi, tokenflux, ovms, or dynamic NewApi provider)
|
||||
```
|
||||
|
||||
**Examples**:
|
||||
|
||||
- `/paintings` → Redirect to user's default painting provider
|
||||
- `/paintings/zhipu` → Zhipu painting page
|
||||
- `/paintings/aihubmix` → Aihubmix painting page
|
||||
- `/paintings/my-custom-provider` → User's custom NewApi provider
|
||||
|
||||
### 5.2 Provider List
|
||||
|
||||
| Provider ID | Component | Description |
|
||||
|-------------|-----------|-------------|
|
||||
| `zhipu` | `ZhipuPage` | Zhipu AI Painting |
|
||||
| `aihubmix` | `AihubmixPage` | Aihubmix Aggregation |
|
||||
| `silicon` | `SiliconPage` | Silicon Flow |
|
||||
| `dmxapi` | `DmxapiPage` | Dmxapi |
|
||||
| `tokenflux` | `TokenFluxPage` | TokenFlux |
|
||||
| `ovms` | `OvmsPage` | OVMS (Local Inference) |
|
||||
| `$providerId` | `NewApiPage` | Dynamic NewApi Provider |
|
||||
|
||||
### 5.3 Component Structure
|
||||
|
||||
```tsx
|
||||
// routes/paintings/route.tsx
|
||||
export const Route = createFileRoute('/paintings')({
|
||||
component: PaintingsLayout,
|
||||
loader: async () => ({
|
||||
providers: await fetchPaintingProviders(),
|
||||
defaultProvider: await getDefaultPaintingProvider()
|
||||
}),
|
||||
})
|
||||
|
||||
function PaintingsLayout() {
|
||||
const { providers } = Route.useLoaderData()
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
{/* Provider selector */}
|
||||
<ProviderSelect providers={providers} />
|
||||
|
||||
{/* Painting content area */}
|
||||
<div className="flex-1">
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 5.4 Special Handling
|
||||
|
||||
- **OVMS Provider**: Only shown in options when local OVMS service is running
|
||||
- **Dynamic Providers**: Custom providers added by users via NewApi, captured using `$providerId`
|
||||
|
||||
---
|
||||
|
||||
## 6. Component Mapping
|
||||
|
||||
| New Route Component | Existing Component | Migration Strategy |
|
||||
|---------------------|-------------------|-------------------|
|
||||
| `ChatLayout` | `HomePage.tsx` | Extract sidebar logic |
|
||||
| `ChatSidebar` | `HomeTabs/index.tsx` | Rename, adjust props |
|
||||
| `ChatView` | `Chat.tsx` | Keep unchanged, adjust data fetching |
|
||||
| `SettingsLayout` | `SettingsPage.tsx` | Extract layout logic |
|
||||
| `NotesLayout` | `NotesSidebar.tsx` | Extract as layout component |
|
||||
| `NotesEditor` | `NotesEditor.tsx` | Keep unchanged |
|
||||
|
||||
---
|
||||
|
||||
## 7. Open Questions
|
||||
|
||||
- [ ] `/` home behavior: Redirect to `/chat` or standalone welcome page?
|
||||
- [ ] Does `/chat/$assistantId` need a dedicated page? Or redirect to first topic directly?
|
||||
- [ ] Left sidebar interaction: Always create new Tab on click? Or reuse existing Tab?
|
||||
- [ ] Tab bar UI details: Close button position, drag-to-reorder, context menu, etc.
|
||||
|
||||
---
|
||||
|
||||
## 8. Changelog
|
||||
|
||||
| Version | Date | Changes |
|
||||
|---------|------|---------|
|
||||
| v0.1.0 | 2025-11-25 | Initial version |
|
||||
@ -2,6 +2,7 @@ import type { BodyForPath, QueryParamsForPath, ResponseForPath } from '@shared/d
|
||||
import type { ConcreteApiPaths } from '@shared/data/api/apiSchemas'
|
||||
import type { PaginatedResponse } from '@shared/data/api/apiTypes'
|
||||
import { useState } from 'react'
|
||||
import type { KeyedMutator } from 'swr'
|
||||
import useSWR, { useSWRConfig } from 'swr'
|
||||
import useSWRMutation from 'swr/mutation'
|
||||
|
||||
@ -139,6 +140,8 @@ export function useQuery<TPath extends ConcreteApiPaths>(
|
||||
error?: Error
|
||||
/** Function to manually refetch data */
|
||||
refetch: () => void
|
||||
/** SWR mutate function for optimistic updates */
|
||||
mutate: KeyedMutator<ResponseForPath<TPath, 'GET'>>
|
||||
} {
|
||||
// Internal type conversion for SWR compatibility
|
||||
const key = options?.enabled !== false ? buildSWRKey(path, options?.query as Record<string, any>) : null
|
||||
@ -160,7 +163,8 @@ export function useQuery<TPath extends ConcreteApiPaths>(
|
||||
data,
|
||||
loading: isLoading || isValidating,
|
||||
error: error as Error | undefined,
|
||||
refetch
|
||||
refetch,
|
||||
mutate
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
122
src/renderer/src/hooks/useTabs.ts
Normal file
122
src/renderer/src/hooks/useTabs.ts
Normal file
@ -0,0 +1,122 @@
|
||||
import { loggerService } from '@logger'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
|
||||
import { useMutation, useQuery } from '../data/hooks/useDataApi'
|
||||
|
||||
const logger = loggerService.withContext('useTabs')
|
||||
|
||||
export type TabType = 'webview' | 'url' | 'browser'
|
||||
|
||||
export interface Tab {
|
||||
id: string
|
||||
type: TabType
|
||||
url: string
|
||||
title: string
|
||||
icon?: string
|
||||
isKeepAlive?: boolean
|
||||
metadata?: Record<string, any>
|
||||
}
|
||||
|
||||
interface TabsState {
|
||||
tabs: Tab[]
|
||||
activeTabId: string
|
||||
}
|
||||
|
||||
const TABS_STORAGE_KEY = 'tabs_state'
|
||||
const DEFAULT_STATE: TabsState = { tabs: [], activeTabId: '' }
|
||||
|
||||
export function useTabs() {
|
||||
// Load state from DB
|
||||
// We cast the path because we haven't fully updated the concrete path types globally yet
|
||||
const {
|
||||
data: tabsState,
|
||||
mutate: mutateState,
|
||||
loading: isLoading
|
||||
} = useQuery(`/app/state/${TABS_STORAGE_KEY}` as any, {
|
||||
swrOptions: {
|
||||
revalidateOnFocus: false,
|
||||
fallbackData: DEFAULT_STATE
|
||||
}
|
||||
})
|
||||
|
||||
// Ensure we always have a valid object structure
|
||||
const currentState: TabsState = useMemo(
|
||||
() => (tabsState && typeof tabsState === 'object' ? (tabsState as TabsState) : DEFAULT_STATE),
|
||||
[tabsState]
|
||||
)
|
||||
const tabs = useMemo(() => (Array.isArray(currentState.tabs) ? currentState.tabs : []), [currentState.tabs])
|
||||
const activeTabId = currentState.activeTabId || ''
|
||||
|
||||
// Mutation for saving
|
||||
const saveMutation = useMutation('PUT', `/app/state/${TABS_STORAGE_KEY}` as any)
|
||||
|
||||
// Unified update helper
|
||||
const updateState = useCallback(
|
||||
async (newState: TabsState) => {
|
||||
// 1. Optimistic update local cache
|
||||
await mutateState(newState, { revalidate: false })
|
||||
|
||||
// 2. Sync to DB
|
||||
saveMutation.mutate({ body: newState }).catch((err) => logger.error('Failed to save tabs state:', err))
|
||||
},
|
||||
[mutateState, saveMutation]
|
||||
)
|
||||
|
||||
const addTab = useCallback(
|
||||
(tab: Tab) => {
|
||||
const exists = tabs.find((t) => t.id === tab.id)
|
||||
if (exists) {
|
||||
updateState({ ...currentState, activeTabId: tab.id })
|
||||
return
|
||||
}
|
||||
const newTabs = [...tabs, tab]
|
||||
updateState({ tabs: newTabs, activeTabId: tab.id })
|
||||
},
|
||||
[tabs, currentState, updateState]
|
||||
)
|
||||
|
||||
const closeTab = useCallback(
|
||||
(id: string) => {
|
||||
const newTabs = tabs.filter((t) => t.id !== id)
|
||||
let newActiveId = activeTabId
|
||||
|
||||
if (activeTabId === id) {
|
||||
// Activate adjacent tab
|
||||
const index = tabs.findIndex((t) => t.id === id)
|
||||
// Try to go left, then right
|
||||
const nextTab = newTabs[index - 1] || newTabs[index]
|
||||
newActiveId = nextTab ? nextTab.id : ''
|
||||
}
|
||||
|
||||
updateState({ tabs: newTabs, activeTabId: newActiveId })
|
||||
},
|
||||
[tabs, activeTabId, updateState]
|
||||
)
|
||||
|
||||
const setActiveTab = useCallback(
|
||||
(id: string) => {
|
||||
if (id !== activeTabId) {
|
||||
updateState({ ...currentState, activeTabId: id })
|
||||
}
|
||||
},
|
||||
[activeTabId, currentState, updateState]
|
||||
)
|
||||
|
||||
const updateTab = useCallback(
|
||||
(id: string, updates: Partial<Tab>) => {
|
||||
const newTabs = tabs.map((t) => (t.id === id ? { ...t, ...updates } : t))
|
||||
updateState({ ...currentState, tabs: newTabs })
|
||||
},
|
||||
[tabs, currentState, updateState]
|
||||
)
|
||||
|
||||
return {
|
||||
tabs,
|
||||
activeTabId,
|
||||
isLoading,
|
||||
addTab,
|
||||
closeTab,
|
||||
setActiveTab,
|
||||
updateTab
|
||||
}
|
||||
}
|
||||
55
src/renderer/src/routeTree.gen.ts
Normal file
55
src/renderer/src/routeTree.gen.ts
Normal file
@ -0,0 +1,55 @@
|
||||
// @ts-nocheck
|
||||
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
|
||||
// This file was automatically generated by TanStack Router.
|
||||
// You should NOT make any changes in this file as it will be overwritten.
|
||||
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
||||
|
||||
import { Route as rootRouteImport } from './routes/__root'
|
||||
import { Route as IndexRouteImport } from './routes/index'
|
||||
|
||||
const IndexRoute = IndexRouteImport.update({
|
||||
id: '/',
|
||||
path: '/',
|
||||
getParentRoute: () => rootRouteImport
|
||||
} as any)
|
||||
|
||||
export interface FileRoutesByFullPath {
|
||||
'/': typeof IndexRoute
|
||||
}
|
||||
export interface FileRoutesByTo {
|
||||
'/': typeof IndexRoute
|
||||
}
|
||||
export interface FileRoutesById {
|
||||
__root__: typeof rootRouteImport
|
||||
'/': typeof IndexRoute
|
||||
}
|
||||
export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath
|
||||
fullPaths: '/'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to: '/'
|
||||
id: '__root__' | '/'
|
||||
fileRoutesById: FileRoutesById
|
||||
}
|
||||
export interface RootRouteChildren {
|
||||
IndexRoute: typeof IndexRoute
|
||||
}
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
interface FileRoutesByPath {
|
||||
'/': {
|
||||
id: '/'
|
||||
path: '/'
|
||||
fullPath: '/'
|
||||
preLoaderRoute: typeof IndexRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const rootRouteChildren: RootRouteChildren = {
|
||||
IndexRoute: IndexRoute
|
||||
}
|
||||
export const routeTree = rootRouteImport._addFileChildren(rootRouteChildren)._addFileTypes<FileRouteTypes>()
|
||||
7
src/renderer/src/routes/__root.tsx
Normal file
7
src/renderer/src/routes/__root.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
import { createRootRoute } from '@tanstack/react-router'
|
||||
|
||||
import { AppShell } from '../components/layout/AppShell'
|
||||
|
||||
export const Route = createRootRoute({
|
||||
component: () => <AppShell />
|
||||
})
|
||||
14
src/renderer/src/routes/index.tsx
Normal file
14
src/renderer/src/routes/index.tsx
Normal file
@ -0,0 +1,14 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
export const Route = createFileRoute('/')({
|
||||
component: Index
|
||||
})
|
||||
|
||||
function Index() {
|
||||
return (
|
||||
<div style={{ padding: 20 }}>
|
||||
<h3>Welcome to Cherry Studio!</h3>
|
||||
<p>Select a tab to start.</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
459
yarn.lock
459
yarn.lock
@ -1545,7 +1545,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/core@npm:^7.21.3":
|
||||
"@babel/core@npm:^7.21.3, @babel/core@npm:^7.23.7, @babel/core@npm:^7.27.4":
|
||||
version: 7.28.5
|
||||
resolution: "@babel/core@npm:7.28.5"
|
||||
dependencies:
|
||||
@ -1591,6 +1591,19 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/generator@npm:^7.27.5, @babel/generator@npm:^7.28.5":
|
||||
version: 7.28.5
|
||||
resolution: "@babel/generator@npm:7.28.5"
|
||||
dependencies:
|
||||
"@babel/parser": "npm:^7.28.5"
|
||||
"@babel/types": "npm:^7.28.5"
|
||||
"@jridgewell/gen-mapping": "npm:^0.3.12"
|
||||
"@jridgewell/trace-mapping": "npm:^0.3.28"
|
||||
jsesc: "npm:^3.0.2"
|
||||
checksum: 10c0/9f219fe1d5431b6919f1a5c60db8d5d34fe546c0d8f5a8511b32f847569234ffc8032beb9e7404649a143f54e15224ecb53a3d11b6bb85c3203e573d91fca752
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/generator@npm:^7.28.0, @babel/generator@npm:^7.28.3":
|
||||
version: 7.28.3
|
||||
resolution: "@babel/generator@npm:7.28.3"
|
||||
@ -1604,16 +1617,12 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/generator@npm:^7.28.5":
|
||||
version: 7.28.5
|
||||
resolution: "@babel/generator@npm:7.28.5"
|
||||
"@babel/helper-annotate-as-pure@npm:^7.27.3":
|
||||
version: 7.27.3
|
||||
resolution: "@babel/helper-annotate-as-pure@npm:7.27.3"
|
||||
dependencies:
|
||||
"@babel/parser": "npm:^7.28.5"
|
||||
"@babel/types": "npm:^7.28.5"
|
||||
"@jridgewell/gen-mapping": "npm:^0.3.12"
|
||||
"@jridgewell/trace-mapping": "npm:^0.3.28"
|
||||
jsesc: "npm:^3.0.2"
|
||||
checksum: 10c0/9f219fe1d5431b6919f1a5c60db8d5d34fe546c0d8f5a8511b32f847569234ffc8032beb9e7404649a143f54e15224ecb53a3d11b6bb85c3203e573d91fca752
|
||||
"@babel/types": "npm:^7.27.3"
|
||||
checksum: 10c0/94996ce0a05b7229f956033e6dcd69393db2b0886d0db6aff41e704390402b8cdcca11f61449cb4f86cfd9e61b5ad3a73e4fa661eeed7846b125bd1c33dbc633
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@ -1630,6 +1639,23 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/helper-create-class-features-plugin@npm:^7.28.5":
|
||||
version: 7.28.5
|
||||
resolution: "@babel/helper-create-class-features-plugin@npm:7.28.5"
|
||||
dependencies:
|
||||
"@babel/helper-annotate-as-pure": "npm:^7.27.3"
|
||||
"@babel/helper-member-expression-to-functions": "npm:^7.28.5"
|
||||
"@babel/helper-optimise-call-expression": "npm:^7.27.1"
|
||||
"@babel/helper-replace-supers": "npm:^7.27.1"
|
||||
"@babel/helper-skip-transparent-expression-wrappers": "npm:^7.27.1"
|
||||
"@babel/traverse": "npm:^7.28.5"
|
||||
semver: "npm:^6.3.1"
|
||||
peerDependencies:
|
||||
"@babel/core": ^7.0.0
|
||||
checksum: 10c0/786a6514efcf4514aaad85beed419b9184d059f4c9a9a95108f320142764999827252a851f7071de19f29424d369616573ecbaa347f1ce23fb12fc6827d9ff56
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/helper-globals@npm:^7.28.0":
|
||||
version: 7.28.0
|
||||
resolution: "@babel/helper-globals@npm:7.28.0"
|
||||
@ -1637,6 +1663,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/helper-member-expression-to-functions@npm:^7.27.1, @babel/helper-member-expression-to-functions@npm:^7.28.5":
|
||||
version: 7.28.5
|
||||
resolution: "@babel/helper-member-expression-to-functions@npm:7.28.5"
|
||||
dependencies:
|
||||
"@babel/traverse": "npm:^7.28.5"
|
||||
"@babel/types": "npm:^7.28.5"
|
||||
checksum: 10c0/4e6e05fbf4dffd0bc3e55e28fcaab008850be6de5a7013994ce874ec2beb90619cda4744b11607a60f8aae0227694502908add6188ceb1b5223596e765b44814
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/helper-module-imports@npm:^7.27.1":
|
||||
version: 7.27.1
|
||||
resolution: "@babel/helper-module-imports@npm:7.27.1"
|
||||
@ -1647,7 +1683,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/helper-module-transforms@npm:^7.28.3":
|
||||
"@babel/helper-module-transforms@npm:^7.27.1, @babel/helper-module-transforms@npm:^7.28.3":
|
||||
version: 7.28.3
|
||||
resolution: "@babel/helper-module-transforms@npm:7.28.3"
|
||||
dependencies:
|
||||
@ -1660,6 +1696,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/helper-optimise-call-expression@npm:^7.27.1":
|
||||
version: 7.27.1
|
||||
resolution: "@babel/helper-optimise-call-expression@npm:7.27.1"
|
||||
dependencies:
|
||||
"@babel/types": "npm:^7.27.1"
|
||||
checksum: 10c0/6b861e7fcf6031b9c9fc2de3cd6c005e94a459d6caf3621d93346b52774925800ca29d4f64595a5ceacf4d161eb0d27649ae385110ed69491d9776686fa488e6
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/helper-plugin-utils@npm:^7.27.1":
|
||||
version: 7.27.1
|
||||
resolution: "@babel/helper-plugin-utils@npm:7.27.1"
|
||||
@ -1667,6 +1712,29 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/helper-replace-supers@npm:^7.27.1":
|
||||
version: 7.27.1
|
||||
resolution: "@babel/helper-replace-supers@npm:7.27.1"
|
||||
dependencies:
|
||||
"@babel/helper-member-expression-to-functions": "npm:^7.27.1"
|
||||
"@babel/helper-optimise-call-expression": "npm:^7.27.1"
|
||||
"@babel/traverse": "npm:^7.27.1"
|
||||
peerDependencies:
|
||||
"@babel/core": ^7.0.0
|
||||
checksum: 10c0/4f2eaaf5fcc196580221a7ccd0f8873447b5d52745ad4096418f6101a1d2e712e9f93722c9a32bc9769a1dc197e001f60d6f5438d4dfde4b9c6a9e4df719354c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/helper-skip-transparent-expression-wrappers@npm:^7.27.1":
|
||||
version: 7.27.1
|
||||
resolution: "@babel/helper-skip-transparent-expression-wrappers@npm:7.27.1"
|
||||
dependencies:
|
||||
"@babel/traverse": "npm:^7.27.1"
|
||||
"@babel/types": "npm:^7.27.1"
|
||||
checksum: 10c0/f625013bcdea422c470223a2614e90d2c1cc9d832e97f32ca1b4f82b34bb4aa67c3904cb4b116375d3b5b753acfb3951ed50835a1e832e7225295c7b0c24dff7
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/helper-string-parser@npm:^7.27.1":
|
||||
version: 7.27.1
|
||||
resolution: "@babel/helper-string-parser@npm:7.27.1"
|
||||
@ -1705,7 +1773,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.28.3, @babel/parser@npm:^7.28.4, @babel/parser@npm:^7.28.5":
|
||||
"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.23.6, @babel/parser@npm:^7.28.3, @babel/parser@npm:^7.28.4, @babel/parser@npm:^7.28.5":
|
||||
version: 7.28.5
|
||||
resolution: "@babel/parser@npm:7.28.5"
|
||||
dependencies:
|
||||
@ -1727,6 +1795,28 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/plugin-syntax-jsx@npm:^7.27.1":
|
||||
version: 7.27.1
|
||||
resolution: "@babel/plugin-syntax-jsx@npm:7.27.1"
|
||||
dependencies:
|
||||
"@babel/helper-plugin-utils": "npm:^7.27.1"
|
||||
peerDependencies:
|
||||
"@babel/core": ^7.0.0-0
|
||||
checksum: 10c0/bc5afe6a458d5f0492c02a54ad98c5756a0c13bd6d20609aae65acd560a9e141b0876da5f358dce34ea136f271c1016df58b461184d7ae9c4321e0f98588bc84
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/plugin-syntax-typescript@npm:^7.27.1":
|
||||
version: 7.27.1
|
||||
resolution: "@babel/plugin-syntax-typescript@npm:7.27.1"
|
||||
dependencies:
|
||||
"@babel/helper-plugin-utils": "npm:^7.27.1"
|
||||
peerDependencies:
|
||||
"@babel/core": ^7.0.0-0
|
||||
checksum: 10c0/11589b4c89c66ef02d57bf56c6246267851ec0c361f58929327dc3e070b0dab644be625bbe7fb4c4df30c3634bfdfe31244e1f517be397d2def1487dbbe3c37d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/plugin-transform-arrow-functions@npm:^7.27.1":
|
||||
version: 7.27.1
|
||||
resolution: "@babel/plugin-transform-arrow-functions@npm:7.27.1"
|
||||
@ -1738,6 +1828,48 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/plugin-transform-modules-commonjs@npm:^7.27.1":
|
||||
version: 7.27.1
|
||||
resolution: "@babel/plugin-transform-modules-commonjs@npm:7.27.1"
|
||||
dependencies:
|
||||
"@babel/helper-module-transforms": "npm:^7.27.1"
|
||||
"@babel/helper-plugin-utils": "npm:^7.27.1"
|
||||
peerDependencies:
|
||||
"@babel/core": ^7.0.0-0
|
||||
checksum: 10c0/4def972dcd23375a266ea1189115a4ff61744b2c9366fc1de648b3fab2c650faf1a94092de93a33ff18858d2e6c4dddeeee5384cb42ba0129baeab01a5cdf1e2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/plugin-transform-typescript@npm:^7.28.5":
|
||||
version: 7.28.5
|
||||
resolution: "@babel/plugin-transform-typescript@npm:7.28.5"
|
||||
dependencies:
|
||||
"@babel/helper-annotate-as-pure": "npm:^7.27.3"
|
||||
"@babel/helper-create-class-features-plugin": "npm:^7.28.5"
|
||||
"@babel/helper-plugin-utils": "npm:^7.27.1"
|
||||
"@babel/helper-skip-transparent-expression-wrappers": "npm:^7.27.1"
|
||||
"@babel/plugin-syntax-typescript": "npm:^7.27.1"
|
||||
peerDependencies:
|
||||
"@babel/core": ^7.0.0-0
|
||||
checksum: 10c0/09e574ba5462e56452b4ceecae65e53c8e697a2d3559ce5d210bed10ac28a18aa69377e7550c30520eb29b40c417ee61997d5d58112657f22983244b78915a7c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/preset-typescript@npm:^7.27.1":
|
||||
version: 7.28.5
|
||||
resolution: "@babel/preset-typescript@npm:7.28.5"
|
||||
dependencies:
|
||||
"@babel/helper-plugin-utils": "npm:^7.27.1"
|
||||
"@babel/helper-validator-option": "npm:^7.27.1"
|
||||
"@babel/plugin-syntax-jsx": "npm:^7.27.1"
|
||||
"@babel/plugin-transform-modules-commonjs": "npm:^7.27.1"
|
||||
"@babel/plugin-transform-typescript": "npm:^7.28.5"
|
||||
peerDependencies:
|
||||
"@babel/core": ^7.0.0-0
|
||||
checksum: 10c0/b3d55548854c105085dd80f638147aa8295bc186d70492289242d6c857cb03a6c61ec15186440ea10ed4a71cdde7d495f5eb3feda46273f36b0ac926e8409629
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.10.4, @babel/runtime@npm:^7.11.1, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.16.7, @babel/runtime@npm:^7.18.0, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.18.6, @babel/runtime@npm:^7.20.0, @babel/runtime@npm:^7.20.13, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.21.0, @babel/runtime@npm:^7.22.5, @babel/runtime@npm:^7.23.2, @babel/runtime@npm:^7.23.6, @babel/runtime@npm:^7.23.9, @babel/runtime@npm:^7.24.4, @babel/runtime@npm:^7.24.7, @babel/runtime@npm:^7.24.8, @babel/runtime@npm:^7.25.7, @babel/runtime@npm:^7.26.0, @babel/runtime@npm:^7.26.7":
|
||||
version: 7.28.3
|
||||
resolution: "@babel/runtime@npm:7.28.3"
|
||||
@ -1763,6 +1895,21 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/traverse@npm:^7.23.7, @babel/traverse@npm:^7.27.7, @babel/traverse@npm:^7.28.5":
|
||||
version: 7.28.5
|
||||
resolution: "@babel/traverse@npm:7.28.5"
|
||||
dependencies:
|
||||
"@babel/code-frame": "npm:^7.27.1"
|
||||
"@babel/generator": "npm:^7.28.5"
|
||||
"@babel/helper-globals": "npm:^7.28.0"
|
||||
"@babel/parser": "npm:^7.28.5"
|
||||
"@babel/template": "npm:^7.27.2"
|
||||
"@babel/types": "npm:^7.28.5"
|
||||
debug: "npm:^4.3.1"
|
||||
checksum: 10c0/f6c4a595993ae2b73f2d4cd9c062f2e232174d293edd4abe1d715bd6281da8d99e47c65857e8d0917d9384c65972f4acdebc6749a7c40a8fcc38b3c7fb3e706f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/traverse@npm:^7.27.1, @babel/traverse@npm:^7.28.0, @babel/traverse@npm:^7.28.3, @babel/traverse@npm:^7.28.4":
|
||||
version: 7.28.4
|
||||
resolution: "@babel/traverse@npm:7.28.4"
|
||||
@ -1778,21 +1925,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/traverse@npm:^7.28.5":
|
||||
version: 7.28.5
|
||||
resolution: "@babel/traverse@npm:7.28.5"
|
||||
dependencies:
|
||||
"@babel/code-frame": "npm:^7.27.1"
|
||||
"@babel/generator": "npm:^7.28.5"
|
||||
"@babel/helper-globals": "npm:^7.28.0"
|
||||
"@babel/parser": "npm:^7.28.5"
|
||||
"@babel/template": "npm:^7.27.2"
|
||||
"@babel/types": "npm:^7.28.5"
|
||||
debug: "npm:^4.3.1"
|
||||
checksum: 10c0/f6c4a595993ae2b73f2d4cd9c062f2e232174d293edd4abe1d715bd6281da8d99e47c65857e8d0917d9384c65972f4acdebc6749a7c40a8fcc38b3c7fb3e706f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/types@npm:^7.0.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.28.4":
|
||||
version: 7.28.4
|
||||
resolution: "@babel/types@npm:7.28.4"
|
||||
@ -1803,7 +1935,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/types@npm:^7.21.3, @babel/types@npm:^7.28.5":
|
||||
"@babel/types@npm:^7.21.3, @babel/types@npm:^7.23.6, @babel/types@npm:^7.27.3, @babel/types@npm:^7.27.7, @babel/types@npm:^7.28.5":
|
||||
version: 7.28.5
|
||||
resolution: "@babel/types@npm:7.28.5"
|
||||
dependencies:
|
||||
@ -11016,6 +11148,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@tanstack/history@npm:1.139.0":
|
||||
version: 1.139.0
|
||||
resolution: "@tanstack/history@npm:1.139.0"
|
||||
checksum: 10c0/000fe41d3c3d7f0384e74fcfb1ecda25800906220925d1ab715e4fad7dab081e81c738238a3c09bfb1203ffd4ab0e1f24da1384ded322a154b9eba58405e3e90
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@tanstack/query-core@npm:5.85.5":
|
||||
version: 5.85.5
|
||||
resolution: "@tanstack/query-core@npm:5.85.5"
|
||||
@ -11034,6 +11173,36 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@tanstack/react-router@npm:^1.139.3":
|
||||
version: 1.139.3
|
||||
resolution: "@tanstack/react-router@npm:1.139.3"
|
||||
dependencies:
|
||||
"@tanstack/history": "npm:1.139.0"
|
||||
"@tanstack/react-store": "npm:^0.8.0"
|
||||
"@tanstack/router-core": "npm:1.139.3"
|
||||
isbot: "npm:^5.1.22"
|
||||
tiny-invariant: "npm:^1.3.3"
|
||||
tiny-warning: "npm:^1.0.3"
|
||||
peerDependencies:
|
||||
react: ">=18.0.0 || >=19.0.0"
|
||||
react-dom: ">=18.0.0 || >=19.0.0"
|
||||
checksum: 10c0/1a145ee628ab43c5ce326dc1e5068508b9b71c3570723b2acee6a11d0487221f704489f9f73a93903cb1cba31985fdadf59922ca2148bf91246cbeb71d64aab9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@tanstack/react-store@npm:^0.8.0":
|
||||
version: 0.8.0
|
||||
resolution: "@tanstack/react-store@npm:0.8.0"
|
||||
dependencies:
|
||||
"@tanstack/store": "npm:0.8.0"
|
||||
use-sync-external-store: "npm:^1.6.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
|
||||
checksum: 10c0/ecf7ad81d97810336d0a808a41442f235a444e98599c6e7e026efd3c4360548b84af9a23612f1d0da85e32a4d9e207632b2ee2cec6f635109a256209caa3bc59
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@tanstack/react-virtual@npm:3.11.3":
|
||||
version: 3.11.3
|
||||
resolution: "@tanstack/react-virtual@npm:3.11.3"
|
||||
@ -11058,6 +11227,99 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@tanstack/router-core@npm:1.139.3":
|
||||
version: 1.139.3
|
||||
resolution: "@tanstack/router-core@npm:1.139.3"
|
||||
dependencies:
|
||||
"@tanstack/history": "npm:1.139.0"
|
||||
"@tanstack/store": "npm:^0.8.0"
|
||||
cookie-es: "npm:^2.0.0"
|
||||
seroval: "npm:^1.4.0"
|
||||
seroval-plugins: "npm:^1.4.0"
|
||||
tiny-invariant: "npm:^1.3.3"
|
||||
tiny-warning: "npm:^1.0.3"
|
||||
checksum: 10c0/1e342c39cb8a9e437671aeaa60c82c11530cd3ae0523983585f7c8d332b5ca471aa73d15ffd242cd4b2c73ecebeee7db8dc1b19cbfc3c211253782df9e4ac9fc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@tanstack/router-generator@npm:1.139.3":
|
||||
version: 1.139.3
|
||||
resolution: "@tanstack/router-generator@npm:1.139.3"
|
||||
dependencies:
|
||||
"@tanstack/router-core": "npm:1.139.3"
|
||||
"@tanstack/router-utils": "npm:1.139.0"
|
||||
"@tanstack/virtual-file-routes": "npm:1.139.0"
|
||||
prettier: "npm:^3.5.0"
|
||||
recast: "npm:^0.23.11"
|
||||
source-map: "npm:^0.7.4"
|
||||
tsx: "npm:^4.19.2"
|
||||
zod: "npm:^3.24.2"
|
||||
checksum: 10c0/6ee595fc7b3da75016129bbf65d8ee1ec50e3c5d96dbabf56718c5ee3332c4f660aa738a7dd8f8207d6aad4e71f3c6c51572f17192db12ada668dc5e491d6757
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@tanstack/router-plugin@npm:^1.139.3":
|
||||
version: 1.139.3
|
||||
resolution: "@tanstack/router-plugin@npm:1.139.3"
|
||||
dependencies:
|
||||
"@babel/core": "npm:^7.27.7"
|
||||
"@babel/plugin-syntax-jsx": "npm:^7.27.1"
|
||||
"@babel/plugin-syntax-typescript": "npm:^7.27.1"
|
||||
"@babel/template": "npm:^7.27.2"
|
||||
"@babel/traverse": "npm:^7.27.7"
|
||||
"@babel/types": "npm:^7.27.7"
|
||||
"@tanstack/router-core": "npm:1.139.3"
|
||||
"@tanstack/router-generator": "npm:1.139.3"
|
||||
"@tanstack/router-utils": "npm:1.139.0"
|
||||
"@tanstack/virtual-file-routes": "npm:1.139.0"
|
||||
babel-dead-code-elimination: "npm:^1.0.10"
|
||||
chokidar: "npm:^3.6.0"
|
||||
unplugin: "npm:^2.1.2"
|
||||
zod: "npm:^3.24.2"
|
||||
peerDependencies:
|
||||
"@rsbuild/core": ">=1.0.2"
|
||||
"@tanstack/react-router": ^1.139.3
|
||||
vite: ">=5.0.0 || >=6.0.0 || >=7.0.0"
|
||||
vite-plugin-solid: ^2.11.10
|
||||
webpack: ">=5.92.0"
|
||||
peerDependenciesMeta:
|
||||
"@rsbuild/core":
|
||||
optional: true
|
||||
"@tanstack/react-router":
|
||||
optional: true
|
||||
vite:
|
||||
optional: true
|
||||
vite-plugin-solid:
|
||||
optional: true
|
||||
webpack:
|
||||
optional: true
|
||||
checksum: 10c0/5795a880e65b27168fb703aff7254955787edde438fcfb381ddd201d74974806880164785b56f055a3c4186331a71ea0ccfa6f1474351cdeaf2608d10f13d2f9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@tanstack/router-utils@npm:1.139.0":
|
||||
version: 1.139.0
|
||||
resolution: "@tanstack/router-utils@npm:1.139.0"
|
||||
dependencies:
|
||||
"@babel/core": "npm:^7.27.4"
|
||||
"@babel/generator": "npm:^7.27.5"
|
||||
"@babel/parser": "npm:^7.27.5"
|
||||
"@babel/preset-typescript": "npm:^7.27.1"
|
||||
ansis: "npm:^4.1.0"
|
||||
diff: "npm:^8.0.2"
|
||||
pathe: "npm:^2.0.3"
|
||||
tinyglobby: "npm:^0.2.15"
|
||||
checksum: 10c0/cb8477ab32f16881b8b92db952d0ab343cb77704ada953e422cee4543dc31aa5efae31295d415ff7deea8f9fadd19ad1a88e26d5f3e0d30a2f6b4099e82d19c9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@tanstack/store@npm:0.8.0, @tanstack/store@npm:^0.8.0":
|
||||
version: 0.8.0
|
||||
resolution: "@tanstack/store@npm:0.8.0"
|
||||
checksum: 10c0/71841a7a7653f744bdea457d2c41768b8d5e5aed1d5ff22bd068e28ced9bf658208c730963809c2223b26b753e19da987c0d98acb7c543abd97de14e0d58991f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@tanstack/virtual-core@npm:3.11.3":
|
||||
version: 3.11.3
|
||||
resolution: "@tanstack/virtual-core@npm:3.11.3"
|
||||
@ -11072,6 +11334,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@tanstack/virtual-file-routes@npm:1.139.0":
|
||||
version: 1.139.0
|
||||
resolution: "@tanstack/virtual-file-routes@npm:1.139.0"
|
||||
checksum: 10c0/abb8173520e133f3e4d8d2eb356059ce0fbc6aa64d55e358cbc810cc323c8848fbd3adf512095bafaf69e7bc01ee588ca5a345d361c42452b8eb41239ce5ed73
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@testing-library/dom@npm:^10.4.0":
|
||||
version: 10.4.0
|
||||
resolution: "@testing-library/dom@npm:10.4.0"
|
||||
@ -12384,12 +12653,12 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/react-dom@npm:^19.0.4":
|
||||
version: 19.1.2
|
||||
resolution: "@types/react-dom@npm:19.1.2"
|
||||
"@types/react-dom@npm:^19.0.4, @types/react-dom@npm:^19.2.3":
|
||||
version: 19.2.3
|
||||
resolution: "@types/react-dom@npm:19.2.3"
|
||||
peerDependencies:
|
||||
"@types/react": ^19.0.0
|
||||
checksum: 10c0/100c341cacba9ec8ae1d47ee051072a3450e9573bf8eeb7262490e341cb246ea0f95a07a1f2077e61cf92648f812a0324c602fcd811bd87b7ce41db2811510cd
|
||||
"@types/react": ^19.2.0
|
||||
checksum: 10c0/b486ebe0f4e2fb35e2e108df1d8fc0927ca5d6002d5771e8a739de11239fe62d0e207c50886185253c99eb9dedfeeb956ea7429e5ba17f6693c7acb4c02f8cd1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@ -12420,21 +12689,12 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/react@npm:*":
|
||||
version: 19.1.12
|
||||
resolution: "@types/react@npm:19.1.12"
|
||||
"@types/react@npm:*, @types/react@npm:^19.0.12, @types/react@npm:^19.2.7":
|
||||
version: 19.2.7
|
||||
resolution: "@types/react@npm:19.2.7"
|
||||
dependencies:
|
||||
csstype: "npm:^3.0.2"
|
||||
checksum: 10c0/e35912b43da0caaab5252444bab87a31ca22950cde2822b3b3dc32e39c2d42dad1a4cf7b5dde9783aa2d007f0b2cba6ab9563fc6d2dbcaaa833b35178118767c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/react@npm:^19.0.12":
|
||||
version: 19.1.2
|
||||
resolution: "@types/react@npm:19.1.2"
|
||||
dependencies:
|
||||
csstype: "npm:^3.0.2"
|
||||
checksum: 10c0/76ffe71395c713d4adc3c759465012d3c956db00af35ab7c6d0d91bd07b274b7ce69caa0478c0760311587bd1e38c78ffc9688ebc629f2b266682a19d8750947
|
||||
csstype: "npm:^3.2.2"
|
||||
checksum: 10c0/a7b75f1f9fcb34badd6f84098be5e35a0aeca614bc91f93d2698664c0b2ba5ad128422bd470ada598238cebe4f9e604a752aead7dc6f5a92261d0c7f9b27cfd1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@ -13738,7 +13998,9 @@ __metadata:
|
||||
"@swc/plugin-styled-components": "npm:^8.0.4"
|
||||
"@tailwindcss/vite": "npm:^4.1.13"
|
||||
"@tanstack/react-query": "npm:^5.85.5"
|
||||
"@tanstack/react-router": "npm:^1.139.3"
|
||||
"@tanstack/react-virtual": "npm:^3.13.12"
|
||||
"@tanstack/router-plugin": "npm:^1.139.3"
|
||||
"@testing-library/dom": "npm:^10.4.0"
|
||||
"@testing-library/jest-dom": "npm:^6.6.3"
|
||||
"@testing-library/react": "npm:^16.3.0"
|
||||
@ -13776,8 +14038,8 @@ __metadata:
|
||||
"@types/mime-types": "npm:^3"
|
||||
"@types/node": "npm:^22.17.1"
|
||||
"@types/pako": "npm:^1.0.2"
|
||||
"@types/react": "npm:^19.0.12"
|
||||
"@types/react-dom": "npm:^19.0.4"
|
||||
"@types/react": "npm:^19.2.7"
|
||||
"@types/react-dom": "npm:^19.2.3"
|
||||
"@types/react-infinite-scroll-component": "npm:^5.0.0"
|
||||
"@types/react-transition-group": "npm:^4.4.12"
|
||||
"@types/react-window": "npm:^1"
|
||||
@ -14641,6 +14903,18 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"babel-dead-code-elimination@npm:^1.0.10":
|
||||
version: 1.0.10
|
||||
resolution: "babel-dead-code-elimination@npm:1.0.10"
|
||||
dependencies:
|
||||
"@babel/core": "npm:^7.23.7"
|
||||
"@babel/parser": "npm:^7.23.6"
|
||||
"@babel/traverse": "npm:^7.23.7"
|
||||
"@babel/types": "npm:^7.23.6"
|
||||
checksum: 10c0/9503662f28cf8f86e7a27c5cc1fa63fc556100cd3bc6f1a4382aa8e9c6df54b15d2e0fcc073016f315d26a9e4004bc4d70829a395f056172b8f9240314da8973
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"bail@npm:^1.0.0":
|
||||
version: 1.0.5
|
||||
resolution: "bail@npm:1.0.5"
|
||||
@ -15348,7 +15622,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"chokidar@npm:^3.5.2":
|
||||
"chokidar@npm:^3.5.2, chokidar@npm:^3.6.0":
|
||||
version: 3.6.0
|
||||
resolution: "chokidar@npm:3.6.0"
|
||||
dependencies:
|
||||
@ -15947,6 +16221,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"cookie-es@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "cookie-es@npm:2.0.0"
|
||||
checksum: 10c0/3b2459030a5ad2bc715aeb27a32f274340670bfc5031ac29e1fba804212517411bb617880d3fe66ace2b64dfb28f3049e2d1ff40d4bec342154ccdd124deaeaa
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"cookie-signature@npm:^1.2.1":
|
||||
version: 1.2.2
|
||||
resolution: "cookie-signature@npm:1.2.2"
|
||||
@ -16198,6 +16479,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"csstype@npm:^3.2.2":
|
||||
version: 3.2.3
|
||||
resolution: "csstype@npm:3.2.3"
|
||||
checksum: 10c0/cd29c51e70fa822f1cecd8641a1445bed7063697469d35633b516e60fe8c1bde04b08f6c5b6022136bb669b64c63d4173af54864510fbb4ee23281801841a3ce
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"csv-parse@npm:^5.6.0":
|
||||
version: 5.6.0
|
||||
resolution: "csv-parse@npm:5.6.0"
|
||||
@ -20602,6 +20890,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"isbot@npm:^5.1.22":
|
||||
version: 5.1.32
|
||||
resolution: "isbot@npm:5.1.32"
|
||||
checksum: 10c0/e5aa9c5c92dae4879cf49956797c46ef77fa919230183cd6254628667ca5e22f15b24bc4d63b0e88cb96da3d7a51e33f847ef7114fa542e3e066f78178c8d97e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"isexe@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "isexe@npm:2.0.0"
|
||||
@ -24725,6 +25020,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"prettier@npm:^3.5.0":
|
||||
version: 3.6.2
|
||||
resolution: "prettier@npm:3.6.2"
|
||||
bin:
|
||||
prettier: bin/prettier.cjs
|
||||
checksum: 10c0/488cb2f2b99ec13da1e50074912870217c11edaddedeadc649b1244c749d15ba94e846423d062e2c4c9ae683e2d65f754de28889ba06e697ac4f988d44f45812
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"pretty-format@npm:^27.0.2":
|
||||
version: 27.5.1
|
||||
resolution: "pretty-format@npm:27.5.1"
|
||||
@ -26153,7 +26457,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"recast@npm:^0.23.5":
|
||||
"recast@npm:^0.23.11, recast@npm:^0.23.5":
|
||||
version: 0.23.11
|
||||
resolution: "recast@npm:0.23.11"
|
||||
dependencies:
|
||||
@ -27197,6 +27501,22 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"seroval-plugins@npm:^1.4.0":
|
||||
version: 1.4.0
|
||||
resolution: "seroval-plugins@npm:1.4.0"
|
||||
peerDependencies:
|
||||
seroval: ^1.0
|
||||
checksum: 10c0/d774b8a23bec45f1fefe314e38e26d2fffc0733ad50253a760a10f46cbb0be3a28ed9fcf60aadc0b3f1d2873f4118453a47e84145e858736944dbcd93b42437e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"seroval@npm:^1.4.0":
|
||||
version: 1.4.0
|
||||
resolution: "seroval@npm:1.4.0"
|
||||
checksum: 10c0/020262db5572c16ae5d22ecefa089112a0b1b9a9c78229dbc9c6059c172ed7f0b5005c7990b80714ff8638ac86274195c2084537e0c2a9178690acacff4b705f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"serve-static@npm:^2.2.0":
|
||||
version: 2.2.0
|
||||
resolution: "serve-static@npm:2.2.0"
|
||||
@ -28500,6 +28820,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tiny-warning@npm:^1.0.3":
|
||||
version: 1.0.3
|
||||
resolution: "tiny-warning@npm:1.0.3"
|
||||
checksum: 10c0/ef8531f581b30342f29670cb41ca248001c6fd7975ce22122bd59b8d62b4fc84ad4207ee7faa95cde982fa3357cd8f4be650142abc22805538c3b1392d7084fa
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tinybench@npm:^2.9.0":
|
||||
version: 2.9.0
|
||||
resolution: "tinybench@npm:2.9.0"
|
||||
@ -28929,7 +29256,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tsx@npm:^4.20.3, tsx@npm:^4.20.6":
|
||||
"tsx@npm:^4.19.2, tsx@npm:^4.20.3, tsx@npm:^4.20.6":
|
||||
version: 4.20.6
|
||||
resolution: "tsx@npm:4.20.6"
|
||||
dependencies:
|
||||
@ -29348,6 +29675,18 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"unplugin@npm:^2.1.2":
|
||||
version: 2.3.11
|
||||
resolution: "unplugin@npm:2.3.11"
|
||||
dependencies:
|
||||
"@jridgewell/remapping": "npm:^2.3.5"
|
||||
acorn: "npm:^8.15.0"
|
||||
picomatch: "npm:^4.0.3"
|
||||
webpack-virtual-modules: "npm:^0.6.2"
|
||||
checksum: 10c0/273c1eab0eca4470c7317428689295c31dbe8ab0b306504de9f03cd20c156debb4131bef24b27ac615862958c5dd950a3951d26c0723ea774652ab3624149cff
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"unplugin@npm:^2.3.5":
|
||||
version: 2.3.10
|
||||
resolution: "unplugin@npm:2.3.10"
|
||||
@ -29489,6 +29828,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"use-sync-external-store@npm:^1.6.0":
|
||||
version: 1.6.0
|
||||
resolution: "use-sync-external-store@npm:1.6.0"
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
checksum: 10c0/35e1179f872a53227bdf8a827f7911da4c37c0f4091c29b76b1e32473d1670ebe7bcd880b808b7549ba9a5605c233350f800ffab963ee4a4ee346ee983b6019b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"utf8-byte-length@npm:^1.0.1":
|
||||
version: 1.0.5
|
||||
resolution: "utf8-byte-length@npm:1.0.5"
|
||||
@ -30482,6 +30830,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"zod@npm:^3.24.2":
|
||||
version: 3.25.76
|
||||
resolution: "zod@npm:3.25.76"
|
||||
checksum: 10c0/5718ec35e3c40b600316c5b4c5e4976f7fee68151bc8f8d90ec18a469be9571f072e1bbaace10f1e85cf8892ea12d90821b200e980ab46916a6166a4260a983c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"zod@npm:^3.25.76 || ^4":
|
||||
version: 4.1.12
|
||||
resolution: "zod@npm:4.1.12"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user