Compare commits

..

4 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
e8fc3fb0a2 test: add tests for HTTP MCP compatibility fix
Co-authored-by: DeJeune <67425183+DeJeune@users.noreply.github.com>
2025-11-01 18:25:18 +00:00
copilot-swe-agent[bot]
4b254637b0 🐛 fix: remove strict mode from MCP tools to fix HTTP MCP errors
Co-authored-by: DeJeune <67425183+DeJeune@users.noreply.github.com>
2025-11-01 18:23:45 +00:00
copilot-swe-agent[bot]
456b709c29 Initial plan 2025-11-01 18:12:43 +00:00
SuYao
28bc89ac7c perf: optimize QR code generation and connection info for phone LAN export (#11086)
* Increase QR code margin for better scanning reliability

- Change QRCodeSVG marginSize from 2 to 4 pixels
- Maintains same QR code size (160px) and error correction level (Q)
- Improves readability and scanning success rate on mobile devices

* Optimize QR code generation and connection info for phone LAN export

- Increase QR code size to 180px and reduce error correction to 'L' for better mobile scanning
- Replace hardcoded logo path with AppLogo config and increase logo size to 60px
- Simplify connection info by removing candidates array and using only essential IP/port data

* Optimize QR code data structure for LAN connection

- Compress IP addresses to numeric format to reduce QR code complexity
- Use compact array format instead of verbose JSON object structure
- Remove debug logging to streamline connection flow

* feat: 更新 WebSocket 状态和候选者响应类型,优化连接信息处理

* Increase QR code size and error correction for better scanning

- Increase QR code size from 180px to 300px for improved readability
- Change error correction level from L (low) to H (high) for better reliability
- Reduce logo size from 60px to 40px to accommodate larger QR data
- Increase margin size from 1 to 2 for better border clearance

* 调整二维码大小和图标尺寸以优化扫描体验

* fix(i18n): Auto update translations for PR #11086

* fix(i18n): Auto update translations for PR #11086

* fix(i18n): Auto update translations for PR #11086

---------

Co-authored-by: GitHub Action <action@github.com>
2025-11-01 12:13:11 +08:00
706 changed files with 7727 additions and 46818 deletions

8
.github/CODEOWNERS vendored
View File

@@ -3,11 +3,3 @@
/src/main/services/ConfigManager.ts @0xfullex
/packages/shared/IpcChannel.ts @0xfullex
/src/main/ipc.ts @0xfullex
/migrations/ @0xfullex
/packages/shared/data/ @0xfullex
/src/main/data/ @0xfullex
/src/renderer/src/data/ @0xfullex
/packages/ui/ @MyPrototypeWhat

View File

@@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest
env:
PRCI: true
if: github.event.pull_request.draft == false || github.head_ref == 'v2'
if: github.event.pull_request.draft == false
steps:
- name: Check out Git repository

View File

@@ -27,7 +27,7 @@
"env": {
"node": true
},
"files": ["src/main/**", "resources/scripts/**", "scripts/**", "playwright.config.ts", "electron.vite.config.ts", "packages/ui/scripts/**"]
"files": ["src/main/**", "resources/scripts/**", "scripts/**", "playwright.config.ts", "electron.vite.config.ts"]
},
{
"env": {
@@ -37,7 +37,6 @@
"src/renderer/**/*.{ts,tsx}",
"packages/aiCore/**",
"packages/extension-table-plus/**",
"packages/ui/**",
"resources/js/**"
]
},

View File

@@ -31,8 +31,7 @@
},
"editor.formatOnSave": true,
"files.associations": {
"*.css": "tailwindcss",
".oxlintrc.json": "jsonc"
"*.css": "tailwindcss"
},
"files.eol": "\n",
// "i18n-ally.displayLanguage": "zh-cn", // 界面显示语言

113
CLAUDE.md
View File

@@ -36,113 +36,14 @@ This file provides guidance to AI coding assistants when working with code in th
- **Renderer Process** (`src/renderer/`): React UI with Redux state management
- **Preload Scripts** (`src/preload/`): Secure IPC bridge
### Key Architectural Components
#### Main Process Services (`src/main/services/`)
- **MCPService**: Model Context Protocol server management
- **KnowledgeService**: Document processing and knowledge base management
- **FileStorage/S3Storage/WebDav**: Multiple storage backends
- **WindowService**: Multi-window management (main, mini, selection windows)
- **ProxyManager**: Network proxy handling
- **SearchService**: Full-text search capabilities
#### AI Core (`src/renderer/src/aiCore/`)
- **Middleware System**: Composable pipeline for AI request processing
- **Client Factory**: Supports multiple AI providers (OpenAI, Anthropic, Gemini, etc.)
- **Stream Processing**: Real-time response handling
#### Data Management
- **Cache System**: Three-layer caching (memory/shared/persist) with React hooks integration
- **Preferences**: Type-safe configuration management with multi-window synchronization
- **User Data**: SQLite-based storage with Drizzle ORM for business data
#### Knowledge Management
- **Embeddings**: Vector search with multiple providers (OpenAI, Voyage, etc.)
- **OCR**: Document text extraction (system OCR, Doc2x, Mineru)
- **Preprocessing**: Document preparation pipeline
- **Loaders**: Support for various file formats (PDF, DOCX, EPUB, etc.)
### Build System
- **Electron-Vite**: Development and build tooling (v4.0.0)
- **Rolldown-Vite**: Using experimental rolldown-vite instead of standard vite
- **Workspaces**: Monorepo structure with `packages/` directory
- **Multiple Entry Points**: Main app, mini window, selection toolbar
- **Styled Components**: CSS-in-JS styling with SWC optimization
### Testing Strategy
- **Vitest**: Unit and integration testing
- **Playwright**: End-to-end testing
- **Component Testing**: React Testing Library
- **Coverage**: Available via `yarn test:coverage`
### Key Patterns
- **IPC Communication**: Secure main-renderer communication via preload scripts
- **Service Layer**: Clear separation between UI and business logic
- **Plugin Architecture**: Extensible via MCP servers and middleware
- **Multi-language Support**: i18n with dynamic loading
- **Theme System**: Light/dark themes with custom CSS variables
### UI Design
The project is in the process of migrating from antd & styled-components to HeroUI. Please use HeroUI to build UI components. The use of antd and styled-components is prohibited.
HeroUI Docs: https://www.heroui.com/docs/guide/introduction
### Database Architecture
- **Database**: SQLite (`cherrystudio.sqlite`) + libsql driver
- **ORM**: Drizzle ORM with comprehensive migration system
- **Schemas**: Located in `src/main/data/db/schemas/` directory
#### Database Standards
- **Table Naming**: Use singular form with snake_case (e.g., `topic`, `message`, `app_state`)
- **Schema Exports**: Export using `xxxTable` pattern (e.g., `topicTable`, `appStateTable`)
- **Field Definition**: Drizzle auto-infers field names, no need to add default field names
- **JSON Fields**: For JSON support, add `{ mode: 'json' }`, refer to `preference.ts` table definition
- **JSON Serialization**: For JSON fields, no need to manually serialize/deserialize when reading/writing to database, Drizzle handles this automatically
- **Timestamps**: Use existing `crudTimestamps` utility
- **Migrations**: Generate via `yarn run migrations:generate`
## Data Access Patterns
The application uses three distinct data management systems. Choose the appropriate system based on data characteristics:
### Cache System
- **Purpose**: Temporary data that can be regenerated
- **Lifecycle**: Component-level (memory), window-level (shared), or persistent (survives restart)
- **Use Cases**: API response caching, computed results, temporary UI state
- **APIs**: `useCache`, `useSharedCache`, `usePersistCache` hooks, or `cacheService`
### Preference System
- **Purpose**: User configuration and application settings
- **Lifecycle**: Permanent until user changes
- **Use Cases**: Theme, language, editor settings, user preferences
- **APIs**: `usePreference`, `usePreferences` hooks, or `preferenceService`
### User Data API
- **Purpose**: Core business data (conversations, files, notes, etc.)
- **Lifecycle**: Permanent business records
- **Use Cases**: Topics, messages, files, knowledge base, user-generated content
- **APIs**: `useDataApi` hook or `dataApiService` for direct calls
### Selection Guidelines
- **Use Cache** for data that can be lost without impact (computed values, API responses)
- **Use Preferences** for user settings that affect app behavior (UI configuration, feature flags)
- **Use User Data API** for irreplaceable business data (conversations, documents, user content)
## Logging Standards
### Usage
### Key Components
- **AI Core** (`src/renderer/src/aiCore/`): Middleware pipeline for multiple AI providers.
- **Services** (`src/main/services/`): MCPService, KnowledgeService, WindowService, etc.
- **Build System**: Electron-Vite with experimental rolldown-vite, yarn workspaces.
- **State Management**: Redux Toolkit (`src/renderer/src/store/`) for predictable state.
- **UI Components**: HeroUI (`@heroui/*`) for all new UI elements.
### Logging
```typescript
import { loggerService } from '@logger'
const logger = loggerService.withContext('moduleName')

View File

@@ -42,7 +42,6 @@
"!.github/**",
"!.husky/**",
"!.vscode/**",
"!.claude/**",
"!*.yaml",
"!*.yml",
"!*.mjs",

View File

@@ -64,10 +64,9 @@ asarUnpack:
- resources/**
- "**/*.{metal,exp,lib}"
- "node_modules/@img/sharp-libvips-*/**"
# copy from node_modules/claude-code-plugins/plugins to resources/data/claude-code-pluginso
extraResources:
- from: "migrations/sqlite-drizzle"
to: "migrations/sqlite-drizzle"
# copy from node_modules/claude-code-plugins/plugins to resources/data/claude-code-pluginso
- from: "./node_modules/claude-code-plugins/plugins/"
to: "claude-code-plugins"

View File

@@ -22,7 +22,6 @@ export default defineConfig({
alias: {
'@main': resolve('src/main'),
'@types': resolve('src/renderer/src/types'),
'@data': resolve('src/main/data'),
'@shared': resolve('packages/shared'),
'@logger': resolve('src/main/services/LoggerService'),
'@mcp-trace/trace-core': resolve('packages/mcp-trace/trace-core'),
@@ -62,20 +61,7 @@ export default defineConfig({
}
},
build: {
sourcemap: isDev,
rollupOptions: {
// Unlike renderer which auto-discovers entries from HTML files,
// preload requires explicit entry point configuration for multiple scripts
input: {
index: resolve(__dirname, 'src/preload/index.ts'),
simplest: resolve(__dirname, 'src/preload/simplest.ts') // Minimal preload
},
external: ['electron'],
output: {
entryFileNames: '[name].js',
format: 'cjs'
}
}
sourcemap: isDev
}
},
renderer: {
@@ -104,14 +90,12 @@ export default defineConfig({
'@shared': resolve('packages/shared'),
'@types': resolve('src/renderer/src/types'),
'@logger': resolve('src/renderer/src/services/LoggerService'),
'@data': resolve('src/renderer/src/data'),
'@mcp-trace/trace-core': resolve('packages/mcp-trace/trace-core'),
'@mcp-trace/trace-web': resolve('packages/mcp-trace/trace-web'),
'@cherrystudio/ai-core/provider': resolve('packages/aiCore/src/core/providers'),
'@cherrystudio/ai-core/built-in/plugins': resolve('packages/aiCore/src/core/plugins/built-in'),
'@cherrystudio/ai-core': resolve('packages/aiCore/src'),
'@cherrystudio/extension-table-plus': resolve('packages/extension-table-plus/src'),
'@cherrystudio/ui': resolve('packages/ui/src')
'@cherrystudio/extension-table-plus': resolve('packages/extension-table-plus/src')
}
},
optimizeDeps: {
@@ -131,8 +115,7 @@ export default defineConfig({
miniWindow: resolve(__dirname, 'src/renderer/miniWindow.html'),
selectionToolbar: resolve(__dirname, 'src/renderer/selectionToolbar.html'),
selectionAction: resolve(__dirname, 'src/renderer/selectionAction.html'),
traceWindow: resolve(__dirname, 'src/renderer/traceWindow.html'),
dataRefactorMigrate: resolve(__dirname, 'src/renderer/dataRefactorMigrate.html')
traceWindow: resolve(__dirname, 'src/renderer/traceWindow.html')
},
onwarn(warning, warn) {
if (warning.code === 'COMMONJS_VARIABLE_IN_ESM') return

View File

@@ -72,9 +72,8 @@ export default defineConfig([
...oxlint.configs['flat/eslint'],
...oxlint.configs['flat/typescript'],
...oxlint.configs['flat/unicorn'],
// Custom rules should be after oxlint to overwrite
// LoggerService Custom Rules - only apply to src directory
{
// LoggerService Custom Rules - only apply to src directory
files: ['src/**/*.{ts,tsx,js,jsx}'],
ignores: ['src/**/__tests__/**', 'src/**/__mocks__/**', 'src/**/*.test.*', 'src/preload/**'],
rules: {
@@ -88,7 +87,6 @@ export default defineConfig([
]
}
},
// i18n
{
files: ['**/*.{ts,tsx,js,jsx}'],
languageOptions: {
@@ -136,30 +134,4 @@ export default defineConfig([
'i18n/no-template-in-t': 'warn'
}
},
// ui migration
{
// Component Rules - prevent importing antd components when migration completed
files: ['**/*.{ts,tsx,js,jsx}'],
ignores: ['src/renderer/src/windows/dataRefactorTest/**/*.{ts,tsx}'],
rules: {
'no-restricted-imports': [
'error',
{
paths: [
{
name: 'antd',
importNames: ['Flex', 'Switch', 'message', 'Button', 'Tooltip'],
message:
'❌ Do not import this component from antd. Use our custom components instead: import { ... } from "@cherrystudio/ui"'
},
// {
// name: '@heroui/react',
// message:
// '❌ Do not import components from heroui directly. Use our wrapped components instead: import { ... } from "@cherrystudio/ui"'
// }
]
}
]
}
},
])

View File

@@ -1,6 +0,0 @@
**THIS DIRECTORY IS NOT FOR RUNTIME USE**
- Using `libsql` as the `sqlite3` driver, and `drizzle` as the ORM and database migration tool
- `migrations/sqlite-drizzle` contains auto-generated migration data. Please **DO NOT** modify it.
- If table structure changes, we should run migrations.
- To generate migrations, use the command `yarn run migrations:generate`

View File

@@ -1,7 +0,0 @@
import { defineConfig } from 'drizzle-kit'
export default defineConfig({
out: './migrations/sqlite-drizzle',
schema: './src/main/data/db/schemas/*',
dialect: 'sqlite',
casing: 'snake_case'
})

View File

@@ -1,17 +0,0 @@
CREATE TABLE `app_state` (
`key` text PRIMARY KEY NOT NULL,
`value` text NOT NULL,
`description` text,
`created_at` integer,
`updated_at` integer
);
--> statement-breakpoint
CREATE TABLE `preference` (
`scope` text NOT NULL,
`key` text NOT NULL,
`value` text,
`created_at` integer,
`updated_at` integer
);
--> statement-breakpoint
CREATE INDEX `scope_name_idx` ON `preference` (`scope`,`key`);

View File

@@ -1,114 +0,0 @@
{
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
},
"dialect": "sqlite",
"enums": {},
"id": "de8009d7-95b9-4f99-99fa-4b8795708f21",
"internal": {
"indexes": {}
},
"prevId": "00000000-0000-0000-0000-000000000000",
"tables": {
"app_state": {
"checkConstraints": {},
"columns": {
"created_at": {
"autoincrement": false,
"name": "created_at",
"notNull": false,
"primaryKey": false,
"type": "integer"
},
"description": {
"autoincrement": false,
"name": "description",
"notNull": false,
"primaryKey": false,
"type": "text"
},
"key": {
"autoincrement": false,
"name": "key",
"notNull": true,
"primaryKey": true,
"type": "text"
},
"updated_at": {
"autoincrement": false,
"name": "updated_at",
"notNull": false,
"primaryKey": false,
"type": "integer"
},
"value": {
"autoincrement": false,
"name": "value",
"notNull": true,
"primaryKey": false,
"type": "text"
}
},
"compositePrimaryKeys": {},
"foreignKeys": {},
"indexes": {},
"name": "app_state",
"uniqueConstraints": {}
},
"preference": {
"checkConstraints": {},
"columns": {
"created_at": {
"autoincrement": false,
"name": "created_at",
"notNull": false,
"primaryKey": false,
"type": "integer"
},
"key": {
"autoincrement": false,
"name": "key",
"notNull": true,
"primaryKey": false,
"type": "text"
},
"scope": {
"autoincrement": false,
"name": "scope",
"notNull": true,
"primaryKey": false,
"type": "text"
},
"updated_at": {
"autoincrement": false,
"name": "updated_at",
"notNull": false,
"primaryKey": false,
"type": "integer"
},
"value": {
"autoincrement": false,
"name": "value",
"notNull": false,
"primaryKey": false,
"type": "text"
}
},
"compositePrimaryKeys": {},
"foreignKeys": {},
"indexes": {
"scope_name_idx": {
"columns": ["scope", "key"],
"isUnique": false,
"name": "scope_name_idx"
}
},
"name": "preference",
"uniqueConstraints": {}
}
},
"version": "6",
"views": {}
}

View File

@@ -1,13 +0,0 @@
{
"dialect": "sqlite",
"entries": [
{
"breakpoints": true,
"idx": 0,
"tag": "0000_solid_lord_hawal",
"version": "6",
"when": 1754745234572
}
],
"version": "7"
}

View File

@@ -1,6 +1,6 @@
{
"name": "CherryStudio",
"version": "2.0.0-alpha",
"version": "1.7.0-beta.3",
"private": true,
"description": "A powerful AI assistant for producer.",
"main": "./out/main/index.js",
@@ -50,10 +50,9 @@
"generate:icons": "electron-icon-builder --input=./build/logo.png --output=build",
"analyze:renderer": "VISUALIZER_RENDERER=true yarn build",
"analyze:main": "VISUALIZER_MAIN=true yarn build",
"typecheck": "concurrently -n \"node,web,ui\" -c \"cyan,magenta,green\" \"npm run typecheck:node\" \"npm run typecheck:web\" \"npm run typecheck:ui\"",
"typecheck": "concurrently -n \"node,web\" -c \"cyan,magenta\" \"npm run typecheck:node\" \"npm run typecheck:web\"",
"typecheck:node": "tsgo --noEmit -p tsconfig.node.json --composite false",
"typecheck:web": "tsgo --noEmit -p tsconfig.web.json --composite false",
"typecheck:ui": "cd packages/ui && npm run type-check",
"check:i18n": "dotenv -e .env -- tsx scripts/check-i18n.ts",
"sync:i18n": "dotenv -e .env -- tsx scripts/sync-i18n.ts",
"update:i18n": "dotenv -e .env -- tsx scripts/update-i18n.ts",
@@ -69,13 +68,11 @@
"test:e2e": "yarn playwright test",
"test:lint": "oxlint --deny-warnings && eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --cache",
"test:scripts": "vitest scripts",
"lint": "oxlint --fix && eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --cache && biome lint --write && biome format --write && yarn typecheck && yarn check:i18n && yarn format:check",
"lint:ox": "oxlint --fix && biome lint --write && biome format --write",
"lint": "oxlint --fix && eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --cache && yarn typecheck && yarn check:i18n && yarn format:check",
"format": "biome format --write && biome lint --write",
"format:check": "biome format && biome lint",
"prepare": "git config blame.ignoreRevsFile .git-blame-ignore-revs && husky",
"claude": "dotenv -e .env -- claude",
"migrations:generate": "drizzle-kit generate --config ./migrations/sqlite-drizzle.config.ts",
"release:aicore:alpha": "yarn workspace @cherrystudio/ai-core version prerelease --immediate && yarn workspace @cherrystudio/ai-core npm publish --tag alpha --access public",
"release:aicore:beta": "yarn workspace @cherrystudio/ai-core version prerelease --immediate && yarn workspace @cherrystudio/ai-core npm publish --tag beta --access public",
"release:aicore": "yarn workspace @cherrystudio/ai-core version patch --immediate && yarn workspace @cherrystudio/ai-core npm publish --access public"
@@ -135,7 +132,6 @@
"@cherrystudio/embedjs-openai": "^0.1.31",
"@cherrystudio/extension-table-plus": "workspace:^",
"@cherrystudio/openai": "^6.5.0",
"@cherrystudio/ui": "workspace:*",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^9.0.0",
"@dnd-kit/sortable": "^10.0.0",
@@ -151,6 +147,7 @@
"@google/genai": "patch:@google/genai@npm%3A1.0.1#~/.yarn/patches/@google-genai-npm-1.0.1-e26f0f9af7.patch",
"@hello-pangea/dnd": "^18.0.1",
"@heroui/react": "^2.8.3",
"@kangfenmao/keyv-storage": "^0.1.0",
"@langchain/community": "^1.0.0",
"@langchain/core": "patch:@langchain/core@npm%3A1.0.2#~/.yarn/patches/@langchain-core-npm-1.0.2-183ef83fe4.patch",
"@langchain/openai": "patch:@langchain/openai@npm%3A1.0.0#~/.yarn/patches/@langchain-openai-npm-1.0.0-474d0ad9d4.patch",

View File

@@ -1,7 +1,8 @@
import type { anthropic } from '@ai-sdk/anthropic'
import type { google } from '@ai-sdk/google'
import type { openai } from '@ai-sdk/openai'
import type { InferToolInput, InferToolOutput, Tool } from 'ai'
import type { InferToolInput, InferToolOutput } from 'ai'
import { type Tool } from 'ai'
import type { ProviderOptionsMap } from '../../../options/types'
import type { OpenRouterSearchConfig } from './openrouter'

View File

@@ -2,7 +2,7 @@ export enum IpcChannel {
App_GetCacheSize = 'app:get-cache-size',
App_ClearCache = 'app:clear-cache',
App_SetLaunchOnBoot = 'app:set-launch-on-boot',
// App_SetLanguage = 'app:set-language',
App_SetLanguage = 'app:set-language',
App_SetEnableSpellCheck = 'app:set-enable-spell-check',
App_SetSpellCheckLanguages = 'app:set-spell-check-languages',
App_CheckForUpdate = 'app:check-for-update',
@@ -14,7 +14,7 @@ export enum IpcChannel {
App_SetLaunchToTray = 'app:set-launch-to-tray',
App_SetTray = 'app:set-tray',
App_SetTrayOnClose = 'app:set-tray-on-close',
// App_SetTheme = 'app:set-theme',
App_SetTheme = 'app:set-theme',
App_SetAutoUpdate = 'app:set-auto-update',
App_SetTestPlan = 'app:set-test-plan',
App_SetTestChannel = 'app:set-test-channel',
@@ -46,7 +46,7 @@ export enum IpcChannel {
App_MacRequestProcessTrust = 'app:mac-request-process-trust',
App_QuoteToMain = 'app:quote-to-main',
// App_SetDisableHardwareAcceleration = 'app:set-disable-hardware-acceleration',
App_SetDisableHardwareAcceleration = 'app:set-disable-hardware-acceleration',
Notification_Send = 'notification:send',
Notification_OnClick = 'notification:on-click',
@@ -225,22 +225,6 @@ export enum IpcChannel {
Backup_DeleteS3File = 'backup:deleteS3File',
Backup_CheckS3Connection = 'backup:checkS3Connection',
// data migration
DataMigrate_CheckNeeded = 'data-migrate:check-needed',
DataMigrate_GetProgress = 'data-migrate:get-progress',
DataMigrate_Cancel = 'data-migrate:cancel',
DataMigrate_RequireBackup = 'data-migrate:require-backup',
DataMigrate_BackupCompleted = 'data-migrate:backup-completed',
DataMigrate_ShowBackupDialog = 'data-migrate:show-backup-dialog',
DataMigrate_StartFlow = 'data-migrate:start-flow',
DataMigrate_ProceedToBackup = 'data-migrate:proceed-to-backup',
DataMigrate_StartMigration = 'data-migrate:start-migration',
DataMigrate_RetryMigration = 'data-migrate:retry-migration',
DataMigrate_RestartApp = 'data-migrate:restart-app',
DataMigrate_CloseWindow = 'data-migrate:close-window',
DataMigrate_SendReduxData = 'data-migrate:send-redux-data',
DataMigrate_GetReduxData = 'data-migrate:get-redux-data',
// zip
Zip_Compress = 'zip:compress',
Zip_Decompress = 'zip:decompress',
@@ -255,8 +239,7 @@ export enum IpcChannel {
// events
BackupProgress = 'backup-progress',
DataMigrateProgress = 'data-migrate-progress',
NativeThemeUpdated = 'native-theme:updated',
ThemeUpdated = 'theme:updated',
RestoreProgress = 'restore-progress',
UpdateError = 'update-error',
UpdateAvailable = 'update-available',
@@ -295,6 +278,12 @@ export enum IpcChannel {
Selection_ToolbarVisibilityChange = 'selection:toolbar-visibility-change',
Selection_ToolbarDetermineSize = 'selection:toolbar-determine-size',
Selection_WriteToClipboard = 'selection:write-to-clipboard',
Selection_SetEnabled = 'selection:set-enabled',
Selection_SetTriggerMode = 'selection:set-trigger-mode',
Selection_SetFilterMode = 'selection:set-filter-mode',
Selection_SetFilterList = 'selection:set-filter-list',
Selection_SetFollowToolbar = 'selection:set-follow-toolbar',
Selection_SetRemeberWinSize = 'selection:set-remeber-win-size',
Selection_ActionWindowClose = 'selection:action-window-close',
Selection_ActionWindowMinimize = 'selection:action-window-minimize',
Selection_ActionWindowPin = 'selection:action-window-pin',
@@ -313,27 +302,6 @@ export enum IpcChannel {
Memory_DeleteAllMemoriesForUser = 'memory:delete-all-memories-for-user',
Memory_GetUsersList = 'memory:get-users-list',
// Data: Preference
Preference_Get = 'preference:get',
Preference_Set = 'preference:set',
Preference_GetMultiple = 'preference:get-multiple',
Preference_SetMultiple = 'preference:set-multiple',
Preference_GetAll = 'preference:get-all',
Preference_Subscribe = 'preference:subscribe',
Preference_Changed = 'preference:changed',
// Data: Cache
Cache_Sync = 'cache:sync',
Cache_SyncBatch = 'cache:sync-batch',
// Data: API Channels
DataApi_Request = 'data-api:request',
DataApi_Batch = 'data-api:batch',
DataApi_Transaction = 'data-api:transaction',
DataApi_Subscribe = 'data-api:subscribe',
DataApi_Unsubscribe = 'data-api:unsubscribe',
DataApi_Stream = 'data-api:stream',
// TRACE
TRACE_SAVE_DATA = 'trace:saveData',
TRACE_GET_DATA = 'trace:getData',

View File

@@ -197,11 +197,11 @@ export enum FeedUrl {
GITHUB_LATEST = 'https://github.com/CherryHQ/cherry-studio/releases/latest/download'
}
// export enum UpgradeChannel {
// LATEST = 'latest', // 最新稳定版本
// RC = 'rc', // 公测版本
// BETA = 'beta' // 预览版本
// }
export enum UpgradeChannel {
LATEST = 'latest', // 最新稳定版本
RC = 'rc', // 公测版本
BETA = 'beta' // 预览版本
}
export const defaultTimeout = 10 * 1000 * 60

View File

@@ -31,3 +31,16 @@ export type WebviewKeyEvent = {
shift: boolean
alt: boolean
}
export interface WebSocketStatusResponse {
isRunning: boolean
port?: number
ip?: string
clientConnected: boolean
}
export interface WebSocketCandidatesResponse {
host: string
interface: string
priority: number
}

View File

@@ -1,106 +0,0 @@
# Cherry Studio Shared Data
This directory contains shared type definitions and schemas for the Cherry Studio data management systems. These files provide type safety and consistency across the entire application.
## 📁 Directory Structure
```
packages/shared/data/
├── api/ # Data API type system
│ ├── index.ts # Barrel exports for clean imports
│ ├── apiSchemas.ts # API endpoint definitions and mappings
│ ├── apiTypes.ts # Core request/response infrastructure types
│ ├── apiModels.ts # Business entity types and DTOs
│ ├── apiPaths.ts # API path definitions and utilities
│ └── errorCodes.ts # Standardized error handling
├── cache/ # Cache system type definitions
│ ├── cacheTypes.ts # Core cache infrastructure types
│ ├── cacheSchemas.ts # Cache key schemas and type mappings
│ └── cacheValueTypes.ts # Cache value type definitions
├── preference/ # Preference system type definitions
│ ├── preferenceTypes.ts # Core preference system types
│ └── preferenceSchemas.ts # Preference schemas and default values
└── README.md # This file
```
## 🏗️ System Overview
This directory provides type definitions for three main data management systems:
### API System (`api/`)
- **Purpose**: Type-safe IPC communication between Main and Renderer processes
- **Features**: RESTful patterns, error handling, business entity definitions
- **Usage**: Ensures type safety for all data API operations
### Cache System (`cache/`)
- **Purpose**: Type definitions for three-layer caching architecture
- **Features**: Memory/shared/persist cache schemas, TTL support, hook integration
- **Usage**: Type-safe caching operations across the application
### Preference System (`preference/`)
- **Purpose**: User configuration and settings management
- **Features**: 158 configuration items, default values, nested key support
- **Usage**: Type-safe preference access and synchronization
## 📋 File Categories
**Framework Infrastructure** - These are TypeScript type definitions that:
- ✅ Exist only at compile time
- ✅ Provide type safety and IntelliSense support
- ✅ Define contracts between application layers
- ✅ Enable static analysis and error detection
## 📖 Usage Examples
### API Types
```typescript
// Import API types
import type { DataRequest, DataResponse, ApiSchemas } from '@shared/data/api'
```
### Cache Types
```typescript
// Import cache types
import type { UseCacheKey, UseSharedCacheKey } from '@shared/data/cache'
```
### Preference Types
```typescript
// Import preference types
import type { PreferenceKeyType, PreferenceDefaultScopeType } from '@shared/data/preference'
```
## 🔧 Development Guidelines
### Adding Cache Types
1. Add cache key to `cache/cacheSchemas.ts`
2. Define value type in `cache/cacheValueTypes.ts`
3. Update type mappings for type safety
### Adding Preference Types
1. Add preference key to `preference/preferenceSchemas.ts`
2. Define default value and type
3. Preference system automatically picks up new keys
### Adding API Types
1. Define business entities in `api/apiModels.ts`
2. Add endpoint definitions to `api/apiSchemas.ts`
3. Export types from `api/index.ts`
### Best Practices
- Use `import type` for type-only imports
- Follow existing naming conventions
- Document complex types with JSDoc
- Maintain type safety across all imports
## 🔗 Related Implementation
### Main Process Services
- `src/main/data/CacheService.ts` - Main process cache management
- `src/main/data/PreferenceService.ts` - Preference management service
- `src/main/data/DataApiService.ts` - Data API coordination service
### Renderer Process Services
- `src/renderer/src/data/CacheService.ts` - Renderer cache service
- `src/renderer/src/data/PreferenceService.ts` - Renderer preference service
- `src/renderer/src/data/DataApiService.ts` - Renderer API client

View File

@@ -1,107 +0,0 @@
/**
* Generic test model definitions
* Contains flexible types for comprehensive API testing
*/
/**
* Generic test item entity - flexible structure for testing various scenarios
*/
export interface TestItem {
/** Unique identifier */
id: string
/** Item title */
title: string
/** Optional description */
description?: string
/** Type category */
type: string
/** Current status */
status: string
/** Priority level */
priority: string
/** Associated tags */
tags: string[]
/** Creation timestamp */
createdAt: string
/** Last update timestamp */
updatedAt: string
/** Additional metadata */
metadata: Record<string, any>
}
/**
* Data Transfer Objects (DTOs) for test operations
*/
/**
* DTO for creating a new test item
*/
export interface CreateTestItemDto {
/** Item title */
title: string
/** Optional description */
description?: string
/** Type category */
type?: string
/** Current status */
status?: string
/** Priority level */
priority?: string
/** Associated tags */
tags?: string[]
/** Additional metadata */
metadata?: Record<string, any>
}
/**
* DTO for updating an existing test item
*/
export interface UpdateTestItemDto {
/** Updated title */
title?: string
/** Updated description */
description?: string
/** Updated type */
type?: string
/** Updated status */
status?: string
/** Updated priority */
priority?: string
/** Updated tags */
tags?: string[]
/** Updated metadata */
metadata?: Record<string, any>
}
/**
* Bulk operation types for batch processing
*/
/**
* Request for bulk operations on multiple items
*/
export interface BulkOperationRequest<TData = any> {
/** Type of bulk operation to perform */
operation: 'create' | 'update' | 'delete' | 'archive' | 'restore'
/** Array of data items to process */
data: TData[]
}
/**
* Response from a bulk operation
*/
export interface BulkOperationResponse {
/** Number of successfully processed items */
successful: number
/** Number of items that failed processing */
failed: number
/** Array of errors that occurred during processing */
errors: Array<{
/** Index of the item that failed */
index: number
/** Error message */
error: string
/** Optional additional error data */
data?: any
}>
}

View File

@@ -1,60 +0,0 @@
import type { ApiSchemas } from './apiSchemas'
/**
* Template literal type utilities for converting parameterized paths to concrete paths
* This enables type-safe API calls with actual paths like '/test/items/123' instead of '/test/items/:id'
*/
/**
* Convert parameterized path templates to concrete path types
* @example '/test/items/:id' -> '/test/items/${string}'
* @example '/topics/:id/messages' -> '/topics/${string}/messages'
*/
export type ResolvedPath<T extends string> = T extends `${infer Prefix}/:${string}/${infer Suffix}`
? `${Prefix}/${string}/${ResolvedPath<Suffix>}`
: T extends `${infer Prefix}/:${string}`
? `${Prefix}/${string}`
: T
/**
* Generate all possible concrete paths from ApiSchemas
* This creates a union type of all valid API paths
*/
export type ConcreteApiPaths = {
[K in keyof ApiSchemas]: ResolvedPath<K & string>
}[keyof ApiSchemas]
/**
* Reverse lookup: from concrete path back to original template path
* Used to determine which ApiSchema entry matches a concrete path
*/
export type MatchApiPath<Path extends string> = {
[K in keyof ApiSchemas]: Path extends ResolvedPath<K & string> ? K : never
}[keyof ApiSchemas]
/**
* Extract query parameters type for a given concrete path
*/
export type QueryParamsForPath<Path extends string> = MatchApiPath<Path> extends keyof ApiSchemas
? ApiSchemas[MatchApiPath<Path>] extends { GET: { query?: infer Q } }
? Q
: Record<string, any>
: Record<string, any>
/**
* Extract request body type for a given concrete path and HTTP method
*/
export type BodyForPath<Path extends string, Method extends string> = MatchApiPath<Path> extends keyof ApiSchemas
? ApiSchemas[MatchApiPath<Path>] extends { [M in Method]: { body: infer B } }
? B
: any
: any
/**
* Extract response type for a given concrete path and HTTP method
*/
export type ResponseForPath<Path extends string, Method extends string> = MatchApiPath<Path> extends keyof ApiSchemas
? ApiSchemas[MatchApiPath<Path>] extends { [M in Method]: { response: infer R } }
? R
: any
: any

View File

@@ -1,487 +0,0 @@
// NOTE: Types are defined inline in the schema for simplicity
// If needed, specific types can be imported from './apiModels'
import type { BodyForPath, ConcreteApiPaths, QueryParamsForPath, ResponseForPath } from './apiPaths'
import type { HttpMethod, PaginatedResponse, PaginationParams } from './apiTypes'
// Re-export for external use
export type { ConcreteApiPaths } from './apiPaths'
/**
* Complete API Schema definitions for Test API
*
* Each path defines the supported HTTP methods with their:
* - Request parameters (params, query, body)
* - Response types
* - Type safety guarantees
*
* This schema serves as the contract between renderer and main processes,
* enabling full TypeScript type checking across IPC boundaries.
*/
export interface ApiSchemas {
/**
* Test items collection endpoint
* @example GET /test/items?page=1&limit=10&search=hello
* @example POST /test/items { "title": "New Test Item" }
*/
'/test/items': {
/** List all test items with optional filtering and pagination */
GET: {
query?: PaginationParams & {
/** Search items by title or description */
search?: string
/** Filter by item type */
type?: string
/** Filter by status */
status?: string
}
response: PaginatedResponse<any>
}
/** Create a new test item */
POST: {
body: {
title: string
description?: string
type?: string
status?: string
priority?: string
tags?: string[]
metadata?: Record<string, any>
}
response: any
}
}
/**
* Individual test item endpoint
* @example GET /test/items/123
* @example PUT /test/items/123 { "title": "Updated Title" }
* @example DELETE /test/items/123
*/
'/test/items/:id': {
/** Get a specific test item by ID */
GET: {
params: { id: string }
response: any
}
/** Update a specific test item */
PUT: {
params: { id: string }
body: {
title?: string
description?: string
type?: string
status?: string
priority?: string
tags?: string[]
metadata?: Record<string, any>
}
response: any
}
/** Delete a specific test item */
DELETE: {
params: { id: string }
response: void
}
}
/**
* Test search endpoint
* @example GET /test/search?query=hello&page=1&limit=20
*/
'/test/search': {
/** Search test items */
GET: {
query: {
/** Search query string */
query: string
/** Page number for pagination */
page?: number
/** Number of results per page */
limit?: number
/** Additional filters */
type?: string
status?: string
}
response: PaginatedResponse<any>
}
}
/**
* Test statistics endpoint
* @example GET /test/stats
*/
'/test/stats': {
/** Get comprehensive test statistics */
GET: {
response: {
/** Total number of items */
total: number
/** Item count grouped by type */
byType: Record<string, number>
/** Item count grouped by status */
byStatus: Record<string, number>
/** Item count grouped by priority */
byPriority: Record<string, number>
/** Recent activity timeline */
recentActivity: Array<{
/** Date of activity */
date: string
/** Number of items on that date */
count: number
}>
}
}
}
/**
* Test bulk operations endpoint
* @example POST /test/bulk { "operation": "create", "data": [...] }
*/
'/test/bulk': {
/** Perform bulk operations on test items */
POST: {
body: {
/** Operation type */
operation: 'create' | 'update' | 'delete'
/** Array of data items to process */
data: any[]
}
response: {
successful: number
failed: number
errors: string[]
}
}
}
/**
* Test error simulation endpoint
* @example POST /test/error { "errorType": "timeout" }
*/
'/test/error': {
/** Simulate various error scenarios for testing */
POST: {
body: {
/** Type of error to simulate */
errorType:
| 'timeout'
| 'network'
| 'server'
| 'notfound'
| 'validation'
| 'unauthorized'
| 'ratelimit'
| 'generic'
}
response: never
}
}
/**
* Test slow response endpoint
* @example POST /test/slow { "delay": 2000 }
*/
'/test/slow': {
/** Test slow response for performance testing */
POST: {
body: {
/** Delay in milliseconds */
delay: number
}
response: {
message: string
delay: number
timestamp: string
}
}
}
/**
* Test data reset endpoint
* @example POST /test/reset
*/
'/test/reset': {
/** Reset all test data to initial state */
POST: {
response: {
message: string
timestamp: string
}
}
}
/**
* Test config endpoint
* @example GET /test/config
* @example PUT /test/config { "setting": "value" }
*/
'/test/config': {
/** Get test configuration */
GET: {
response: Record<string, any>
}
/** Update test configuration */
PUT: {
body: Record<string, any>
response: Record<string, any>
}
}
/**
* Test status endpoint
* @example GET /test/status
*/
'/test/status': {
/** Get system test status */
GET: {
response: {
status: string
timestamp: string
version: string
uptime: number
environment: string
}
}
}
/**
* Test performance endpoint
* @example GET /test/performance
*/
'/test/performance': {
/** Get performance metrics */
GET: {
response: {
requestsPerSecond: number
averageLatency: number
memoryUsage: number
cpuUsage: number
uptime: number
}
}
}
/**
* Batch execution of multiple requests
* @example POST /batch { "requests": [...], "parallel": true }
*/
'/batch': {
/** Execute multiple API requests in a single call */
POST: {
body: {
/** Array of requests to execute */
requests: Array<{
/** HTTP method for the request */
method: HttpMethod
/** API path for the request */
path: string
/** URL parameters */
params?: any
/** Request body */
body?: any
}>
/** Execute requests in parallel vs sequential */
parallel?: boolean
}
response: {
/** Results array matching input order */
results: Array<{
/** HTTP status code */
status: number
/** Response data if successful */
data?: any
/** Error information if failed */
error?: any
}>
/** Batch execution metadata */
metadata: {
/** Total execution duration in ms */
duration: number
/** Number of successful requests */
successCount: number
/** Number of failed requests */
errorCount: number
}
}
}
}
/**
* Atomic transaction of multiple operations
* @example POST /transaction { "operations": [...], "options": { "rollbackOnError": true } }
*/
'/transaction': {
/** Execute multiple operations in a database transaction */
POST: {
body: {
/** Array of operations to execute atomically */
operations: Array<{
/** HTTP method for the operation */
method: HttpMethod
/** API path for the operation */
path: string
/** URL parameters */
params?: any
/** Request body */
body?: any
}>
/** Transaction configuration options */
options?: {
/** Database isolation level */
isolation?: 'read-uncommitted' | 'read-committed' | 'repeatable-read' | 'serializable'
/** Rollback all operations on any error */
rollbackOnError?: boolean
/** Transaction timeout in milliseconds */
timeout?: number
}
}
response: Array<{
/** HTTP status code */
status: number
/** Response data if successful */
data?: any
/** Error information if failed */
error?: any
}>
}
}
}
/**
* Simplified type extraction helpers
*/
export type ApiPaths = keyof ApiSchemas
export type ApiMethods<TPath extends ApiPaths> = keyof ApiSchemas[TPath] & HttpMethod
export type ApiResponse<TPath extends ApiPaths, TMethod extends string> = TPath extends keyof ApiSchemas
? TMethod extends keyof ApiSchemas[TPath]
? ApiSchemas[TPath][TMethod] extends { response: infer R }
? R
: never
: never
: never
export type ApiParams<TPath extends ApiPaths, TMethod extends string> = TPath extends keyof ApiSchemas
? TMethod extends keyof ApiSchemas[TPath]
? ApiSchemas[TPath][TMethod] extends { params: infer P }
? P
: never
: never
: never
export type ApiQuery<TPath extends ApiPaths, TMethod extends string> = TPath extends keyof ApiSchemas
? TMethod extends keyof ApiSchemas[TPath]
? ApiSchemas[TPath][TMethod] extends { query: infer Q }
? Q
: never
: never
: never
export type ApiBody<TPath extends ApiPaths, TMethod extends string> = TPath extends keyof ApiSchemas
? TMethod extends keyof ApiSchemas[TPath]
? ApiSchemas[TPath][TMethod] extends { body: infer B }
? B
: never
: never
: never
/**
* Type-safe API client interface using concrete paths
* Accepts actual paths like '/test/items/123' instead of '/test/items/:id'
* Automatically infers query, body, and response types from ApiSchemas
*/
export interface ApiClient {
get<TPath extends ConcreteApiPaths>(
path: TPath,
options?: {
query?: QueryParamsForPath<TPath>
headers?: Record<string, string>
}
): Promise<ResponseForPath<TPath, 'GET'>>
post<TPath extends ConcreteApiPaths>(
path: TPath,
options: {
body?: BodyForPath<TPath, 'POST'>
query?: Record<string, any>
headers?: Record<string, string>
}
): Promise<ResponseForPath<TPath, 'POST'>>
put<TPath extends ConcreteApiPaths>(
path: TPath,
options: {
body: BodyForPath<TPath, 'PUT'>
query?: Record<string, any>
headers?: Record<string, string>
}
): Promise<ResponseForPath<TPath, 'PUT'>>
delete<TPath extends ConcreteApiPaths>(
path: TPath,
options?: {
query?: Record<string, any>
headers?: Record<string, string>
}
): Promise<ResponseForPath<TPath, 'DELETE'>>
patch<TPath extends ConcreteApiPaths>(
path: TPath,
options: {
body?: BodyForPath<TPath, 'PATCH'>
query?: Record<string, any>
headers?: Record<string, string>
}
): Promise<ResponseForPath<TPath, 'PATCH'>>
}
/**
* Helper types to determine if parameters are required based on schema
*/
type HasRequiredQuery<Path extends ApiPaths, Method extends ApiMethods<Path>> = Path extends keyof ApiSchemas
? Method extends keyof ApiSchemas[Path]
? ApiSchemas[Path][Method] extends { query: any }
? true
: false
: false
: false
type HasRequiredBody<Path extends ApiPaths, Method extends ApiMethods<Path>> = Path extends keyof ApiSchemas
? Method extends keyof ApiSchemas[Path]
? ApiSchemas[Path][Method] extends { body: any }
? true
: false
: false
: false
type HasRequiredParams<Path extends ApiPaths, Method extends ApiMethods<Path>> = Path extends keyof ApiSchemas
? Method extends keyof ApiSchemas[Path]
? ApiSchemas[Path][Method] extends { params: any }
? true
: false
: false
: false
/**
* Handler function for a specific API endpoint
* Provides type-safe parameter extraction based on ApiSchemas
* Parameters are required or optional based on the schema definition
*/
export type ApiHandler<Path extends ApiPaths, Method extends ApiMethods<Path>> = (
params: (HasRequiredParams<Path, Method> extends true
? { params: ApiParams<Path, Method> }
: { params?: ApiParams<Path, Method> }) &
(HasRequiredQuery<Path, Method> extends true
? { query: ApiQuery<Path, Method> }
: { query?: ApiQuery<Path, Method> }) &
(HasRequiredBody<Path, Method> extends true ? { body: ApiBody<Path, Method> } : { body?: ApiBody<Path, Method> })
) => Promise<ApiResponse<Path, Method>>
/**
* Complete API implementation that must match ApiSchemas structure
* TypeScript will error if any endpoint is missing - this ensures exhaustive coverage
*/
export type ApiImplementation = {
[Path in ApiPaths]: {
[Method in ApiMethods<Path>]: ApiHandler<Path, Method>
}
}

View File

@@ -1,289 +0,0 @@
/**
* Core types for the Data API system
* Provides type definitions for request/response handling across renderer-main IPC communication
*/
/**
* Standard HTTP methods supported by the Data API
*/
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'
/**
* Request object structure for Data API calls
*/
export interface DataRequest<T = any> {
/** Unique request identifier for tracking and correlation */
id: string
/** HTTP method for the request */
method: HttpMethod
/** API path (e.g., '/topics', '/topics/123') */
path: string
/** URL parameters for the request */
params?: Record<string, any>
/** Request body data */
body?: T
/** Request headers */
headers?: Record<string, string>
/** Additional metadata for request processing */
metadata?: {
/** Request timestamp */
timestamp: number
/** OpenTelemetry span context for tracing */
spanContext?: any
/** Cache options for this specific request */
cache?: CacheOptions
}
}
/**
* Response object structure for Data API calls
*/
export interface DataResponse<T = any> {
/** Request ID that this response corresponds to */
id: string
/** HTTP status code */
status: number
/** Response data if successful */
data?: T
/** Error information if request failed */
error?: DataApiError
/** Response metadata */
metadata?: {
/** Request processing duration in milliseconds */
duration: number
/** Whether response was served from cache */
cached?: boolean
/** Cache TTL if applicable */
cacheTtl?: number
/** Response timestamp */
timestamp: number
}
}
/**
* Standardized error structure for Data API
*/
export interface DataApiError {
/** Error code for programmatic handling */
code: string
/** Human-readable error message */
message: string
/** HTTP status code */
status: number
/** Additional error details */
details?: any
/** Error stack trace (development mode only) */
stack?: string
}
/**
* Standard error codes for Data API
*/
export enum ErrorCode {
// Client errors (4xx)
BAD_REQUEST = 'BAD_REQUEST',
UNAUTHORIZED = 'UNAUTHORIZED',
FORBIDDEN = 'FORBIDDEN',
NOT_FOUND = 'NOT_FOUND',
METHOD_NOT_ALLOWED = 'METHOD_NOT_ALLOWED',
VALIDATION_ERROR = 'VALIDATION_ERROR',
RATE_LIMIT_EXCEEDED = 'RATE_LIMIT_EXCEEDED',
// Server errors (5xx)
INTERNAL_SERVER_ERROR = 'INTERNAL_SERVER_ERROR',
DATABASE_ERROR = 'DATABASE_ERROR',
SERVICE_UNAVAILABLE = 'SERVICE_UNAVAILABLE',
// Custom application errors
MIGRATION_ERROR = 'MIGRATION_ERROR',
PERMISSION_DENIED = 'PERMISSION_DENIED',
RESOURCE_LOCKED = 'RESOURCE_LOCKED',
CONCURRENT_MODIFICATION = 'CONCURRENT_MODIFICATION'
}
/**
* Cache configuration options
*/
export interface CacheOptions {
/** Cache TTL in seconds */
ttl?: number
/** Return stale data while revalidating in background */
staleWhileRevalidate?: boolean
/** Custom cache key override */
cacheKey?: string
/** Operations that should invalidate this cache entry */
invalidateOn?: string[]
/** Whether to bypass cache entirely */
noCache?: boolean
}
/**
* Transaction request wrapper for atomic operations
*/
export interface TransactionRequest {
/** List of operations to execute in transaction */
operations: DataRequest[]
/** Transaction options */
options?: {
/** Database isolation level */
isolation?: 'read-uncommitted' | 'read-committed' | 'repeatable-read' | 'serializable'
/** Whether to rollback entire transaction on any error */
rollbackOnError?: boolean
/** Transaction timeout in milliseconds */
timeout?: number
}
}
/**
* Batch request for multiple operations
*/
export interface BatchRequest {
/** List of requests to execute */
requests: DataRequest[]
/** Whether to execute requests in parallel */
parallel?: boolean
/** Stop on first error */
stopOnError?: boolean
}
/**
* Batch response containing results for all requests
*/
export interface BatchResponse {
/** Individual response for each request */
results: DataResponse[]
/** Overall batch execution metadata */
metadata: {
/** Total execution time */
duration: number
/** Number of successful operations */
successCount: number
/** Number of failed operations */
errorCount: number
}
}
/**
* Pagination parameters for list operations
*/
export interface PaginationParams {
/** Page number (1-based) */
page?: number
/** Items per page */
limit?: number
/** Cursor for cursor-based pagination */
cursor?: string
/** Sort field and direction */
sort?: {
field: string
order: 'asc' | 'desc'
}
}
/**
* Paginated response wrapper
*/
export interface PaginatedResponse<T> {
/** Items for current page */
items: T[]
/** Total number of items */
total: number
/** Current page number */
page: number
/** Total number of pages */
pageCount: number
/** Whether there are more pages */
hasNext: boolean
/** Whether there are previous pages */
hasPrev: boolean
/** Next cursor for cursor-based pagination */
nextCursor?: string
/** Previous cursor for cursor-based pagination */
prevCursor?: string
}
/**
* Subscription options for real-time data updates
*/
export interface SubscriptionOptions {
/** Path pattern to subscribe to */
path: string
/** Filters to apply to subscription */
filters?: Record<string, any>
/** Whether to receive initial data */
includeInitial?: boolean
/** Custom subscription metadata */
metadata?: Record<string, any>
}
/**
* Subscription callback function
*/
export type SubscriptionCallback<T = any> = (data: T, event: SubscriptionEvent) => void
/**
* Subscription event types
*/
export enum SubscriptionEvent {
CREATED = 'created',
UPDATED = 'updated',
DELETED = 'deleted',
INITIAL = 'initial',
ERROR = 'error'
}
/**
* Middleware interface
*/
export interface Middleware {
/** Middleware name */
name: string
/** Execution priority (lower = earlier) */
priority?: number
/** Middleware execution function */
execute(req: DataRequest, res: DataResponse, next: () => Promise<void>): Promise<void>
}
/**
* Request context passed through middleware chain
*/
export interface RequestContext {
/** Original request */
request: DataRequest
/** Response being built */
response: DataResponse
/** Path that matched this request */
path?: string
/** HTTP method */
method?: HttpMethod
/** Authenticated user (if any) */
user?: any
/** Additional context data */
data: Map<string, any>
}
/**
* Base options for service operations
*/
export interface ServiceOptions {
/** Database transaction to use */
transaction?: any
/** User context for authorization */
user?: any
/** Additional service-specific options */
metadata?: Record<string, any>
}
/**
* Standard service response wrapper
*/
export interface ServiceResult<T = any> {
/** Whether operation was successful */
success: boolean
/** Result data if successful */
data?: T
/** Error information if failed */
error?: DataApiError
/** Additional metadata */
metadata?: Record<string, any>
}

View File

@@ -1,194 +0,0 @@
/**
* Centralized error code definitions for the Data API system
* Provides consistent error handling across renderer and main processes
*/
import type { DataApiError } from './apiTypes'
import { ErrorCode } from './apiTypes'
// Re-export ErrorCode for convenience
export { ErrorCode } from './apiTypes'
/**
* Error code to HTTP status mapping
*/
export const ERROR_STATUS_MAP: Record<ErrorCode, number> = {
// Client errors (4xx)
[ErrorCode.BAD_REQUEST]: 400,
[ErrorCode.UNAUTHORIZED]: 401,
[ErrorCode.FORBIDDEN]: 403,
[ErrorCode.NOT_FOUND]: 404,
[ErrorCode.METHOD_NOT_ALLOWED]: 405,
[ErrorCode.VALIDATION_ERROR]: 422,
[ErrorCode.RATE_LIMIT_EXCEEDED]: 429,
// Server errors (5xx)
[ErrorCode.INTERNAL_SERVER_ERROR]: 500,
[ErrorCode.DATABASE_ERROR]: 500,
[ErrorCode.SERVICE_UNAVAILABLE]: 503,
// Custom application errors (5xx)
[ErrorCode.MIGRATION_ERROR]: 500,
[ErrorCode.PERMISSION_DENIED]: 403,
[ErrorCode.RESOURCE_LOCKED]: 423,
[ErrorCode.CONCURRENT_MODIFICATION]: 409
}
/**
* Default error messages for each error code
*/
export const ERROR_MESSAGES: Record<ErrorCode, string> = {
[ErrorCode.BAD_REQUEST]: 'Bad request: Invalid request format or parameters',
[ErrorCode.UNAUTHORIZED]: 'Unauthorized: Authentication required',
[ErrorCode.FORBIDDEN]: 'Forbidden: Insufficient permissions',
[ErrorCode.NOT_FOUND]: 'Not found: Requested resource does not exist',
[ErrorCode.METHOD_NOT_ALLOWED]: 'Method not allowed: HTTP method not supported for this endpoint',
[ErrorCode.VALIDATION_ERROR]: 'Validation error: Request data does not meet requirements',
[ErrorCode.RATE_LIMIT_EXCEEDED]: 'Rate limit exceeded: Too many requests',
[ErrorCode.INTERNAL_SERVER_ERROR]: 'Internal server error: An unexpected error occurred',
[ErrorCode.DATABASE_ERROR]: 'Database error: Failed to access or modify data',
[ErrorCode.SERVICE_UNAVAILABLE]: 'Service unavailable: The service is temporarily unavailable',
[ErrorCode.MIGRATION_ERROR]: 'Migration error: Failed to migrate data',
[ErrorCode.PERMISSION_DENIED]: 'Permission denied: Operation not allowed for current user',
[ErrorCode.RESOURCE_LOCKED]: 'Resource locked: Resource is currently locked by another operation',
[ErrorCode.CONCURRENT_MODIFICATION]: 'Concurrent modification: Resource was modified by another user'
}
/**
* Utility class for creating standardized Data API errors
*/
export class DataApiErrorFactory {
/**
* Create a DataApiError with standard properties
*/
static create(code: ErrorCode, customMessage?: string, details?: any, stack?: string): DataApiError {
return {
code,
message: customMessage || ERROR_MESSAGES[code],
status: ERROR_STATUS_MAP[code],
details,
stack: stack || undefined
}
}
/**
* Create a validation error with field-specific details
*/
static validation(fieldErrors: Record<string, string[]>, message?: string): DataApiError {
return this.create(ErrorCode.VALIDATION_ERROR, message || 'Request validation failed', { fieldErrors })
}
/**
* Create a not found error for specific resource
*/
static notFound(resource: string, id?: string): DataApiError {
const message = id ? `${resource} with id '${id}' not found` : `${resource} not found`
return this.create(ErrorCode.NOT_FOUND, message, { resource, id })
}
/**
* Create a database error with query details
*/
static database(originalError: Error, operation?: string): DataApiError {
return this.create(
ErrorCode.DATABASE_ERROR,
`Database operation failed${operation ? `: ${operation}` : ''}`,
{
originalError: originalError.message,
operation
},
originalError.stack
)
}
/**
* Create a permission denied error
*/
static permissionDenied(action: string, resource?: string): DataApiError {
const message = resource ? `Permission denied: Cannot ${action} ${resource}` : `Permission denied: Cannot ${action}`
return this.create(ErrorCode.PERMISSION_DENIED, message, { action, resource })
}
/**
* Create an internal server error from an unexpected error
*/
static internal(originalError: Error, context?: string): DataApiError {
const message = context
? `Internal error in ${context}: ${originalError.message}`
: `Internal error: ${originalError.message}`
return this.create(
ErrorCode.INTERNAL_SERVER_ERROR,
message,
{ originalError: originalError.message, context },
originalError.stack
)
}
/**
* Create a rate limit exceeded error
*/
static rateLimit(limit: number, windowMs: number): DataApiError {
return this.create(ErrorCode.RATE_LIMIT_EXCEEDED, `Rate limit exceeded: ${limit} requests per ${windowMs}ms`, {
limit,
windowMs
})
}
/**
* Create a resource locked error
*/
static resourceLocked(resource: string, id: string, lockedBy?: string): DataApiError {
const message = lockedBy
? `${resource} '${id}' is locked by ${lockedBy}`
: `${resource} '${id}' is currently locked`
return this.create(ErrorCode.RESOURCE_LOCKED, message, { resource, id, lockedBy })
}
/**
* Create a concurrent modification error
*/
static concurrentModification(resource: string, id: string): DataApiError {
return this.create(ErrorCode.CONCURRENT_MODIFICATION, `${resource} '${id}' was modified by another user`, {
resource,
id
})
}
}
/**
* Check if an error is a Data API error
*/
export function isDataApiError(error: any): error is DataApiError {
return (
error &&
typeof error === 'object' &&
typeof error.code === 'string' &&
typeof error.message === 'string' &&
typeof error.status === 'number'
)
}
/**
* Convert a generic error to a DataApiError
*/
export function toDataApiError(error: unknown, context?: string): DataApiError {
if (isDataApiError(error)) {
return error
}
if (error instanceof Error) {
return DataApiErrorFactory.internal(error, context)
}
return DataApiErrorFactory.create(
ErrorCode.INTERNAL_SERVER_ERROR,
`Unknown error${context ? ` in ${context}` : ''}: ${String(error)}`,
{ originalError: error, context }
)
}

View File

@@ -1,121 +0,0 @@
/**
* Cherry Studio Data API - Barrel Exports
*
* This file provides a centralized entry point for all data API types,
* schemas, and utilities. Import everything you need from this single location.
*
* @example
* ```typescript
* import { Topic, CreateTopicDto, ApiSchemas, DataRequest, ErrorCode } from '@/shared/data'
* ```
*/
// Core data API types and infrastructure
export type {
BatchRequest,
BatchResponse,
CacheOptions,
DataApiError,
DataRequest,
DataResponse,
HttpMethod,
Middleware,
PaginatedResponse,
PaginationParams,
RequestContext,
ServiceOptions,
ServiceResult,
SubscriptionCallback,
SubscriptionOptions,
TransactionRequest
} from './apiTypes'
export { ErrorCode, SubscriptionEvent } from './apiTypes'
// Domain models and DTOs
export type {
BulkOperationRequest,
BulkOperationResponse,
CreateTestItemDto,
TestItem,
UpdateTestItemDto
} from './apiModels'
// API schema definitions and type helpers
export type {
ApiBody,
ApiClient,
ApiMethods,
ApiParams,
ApiPaths,
ApiQuery,
ApiResponse,
ApiSchemas
} from './apiSchemas'
// Path type utilities for template literal types
export type {
BodyForPath,
ConcreteApiPaths,
MatchApiPath,
QueryParamsForPath,
ResolvedPath,
ResponseForPath
} from './apiPaths'
// Error handling utilities
export {
ErrorCode as DataApiErrorCode,
DataApiErrorFactory,
ERROR_MESSAGES,
ERROR_STATUS_MAP,
isDataApiError,
toDataApiError
} from './errorCodes'
/**
* Re-export commonly used type combinations for convenience
*/
// Import types for re-export convenience types
import type { CreateTestItemDto, TestItem, UpdateTestItemDto } from './apiModels'
import type {
BatchRequest,
BatchResponse,
DataApiError,
DataRequest,
DataResponse,
ErrorCode,
PaginatedResponse,
PaginationParams,
TransactionRequest
} from './apiTypes'
import type { DataApiErrorFactory } from './errorCodes'
/** All test item-related types */
export type TestItemTypes = {
TestItem: TestItem
CreateTestItemDto: CreateTestItemDto
UpdateTestItemDto: UpdateTestItemDto
}
/** All error-related types and utilities */
export type ErrorTypes = {
DataApiError: DataApiError
ErrorCode: ErrorCode
ErrorFactory: typeof DataApiErrorFactory
}
/** All request/response types */
export type RequestTypes = {
DataRequest: DataRequest
DataResponse: DataResponse
BatchRequest: BatchRequest
BatchResponse: BatchResponse
TransactionRequest: TransactionRequest
}
/** All pagination-related types */
export type PaginationTypes = {
PaginationParams: PaginationParams
PaginatedResponse: PaginatedResponse<any>
}

View File

@@ -1,144 +0,0 @@
import type * as CacheValueTypes from './cacheValueTypes'
/**
* Use cache schema for renderer hook
*/
export type UseCacheSchema = {
// App state
'app.dist.update_state': CacheValueTypes.CacheAppUpdateState
'app.user.avatar': string
// Chat context
'chat.multi_select_mode': boolean
'chat.selected_message_ids': string[]
'chat.generating': boolean
'chat.websearch.searching': boolean
'chat.websearch.active_searches': CacheValueTypes.CacheActiveSearches
// Minapp management
'minapp.opened_keep_alive': CacheValueTypes.CacheMinAppType[]
'minapp.current_id': string
'minapp.show': boolean
'minapp.opened_oneoff': CacheValueTypes.CacheMinAppType | null
// Topic management
'topic.active': CacheValueTypes.CacheTopic | null
'topic.renaming': string[]
'topic.newly_renamed': string[]
// Test keys (for dataRefactorTest window)
// TODO: remove after testing
'test-hook-memory-1': string
'test-ttl-cache': string
'test-protected-cache': string
'test-deep-equal': { nested: { count: number }; tags: string[] }
'test-performance': number
'test-multi-hook': string
'concurrent-test-1': number
'concurrent-test-2': number
'large-data-test': Record<string, any>
'test-number-cache': number
'test-object-cache': { name: string; count: number; active: boolean }
}
export const DefaultUseCache: UseCacheSchema = {
// App state
'app.dist.update_state': {
info: null,
checking: false,
downloading: false,
downloaded: false,
downloadProgress: 0,
available: false
},
'app.user.avatar': '',
// Chat context
'chat.multi_select_mode': false,
'chat.selected_message_ids': [],
'chat.generating': false,
'chat.websearch.searching': false,
'chat.websearch.active_searches': {},
// Minapp management
'minapp.opened_keep_alive': [],
'minapp.current_id': '',
'minapp.show': false,
'minapp.opened_oneoff': null,
// Topic management
'topic.active': null,
'topic.renaming': [],
'topic.newly_renamed': [],
// Test keys (for dataRefactorTest window)
// TODO: remove after testing
'test-hook-memory-1': 'default-memory-value',
'test-ttl-cache': 'test-ttl-cache',
'test-protected-cache': 'protected-value',
'test-deep-equal': { nested: { count: 0 }, tags: ['initial'] },
'test-performance': 0,
'test-multi-hook': 'hook-1-default',
'concurrent-test-1': 0,
'concurrent-test-2': 0,
'large-data-test': {},
'test-number-cache': 42,
'test-object-cache': { name: 'test', count: 0, active: true }
}
/**
* Use shared cache schema for renderer hook
*/
export type UseSharedCacheSchema = {
'example-key': string
// Test keys (for dataRefactorTest window)
// TODO: remove after testing
'test-hook-shared-1': string
'test-multi-hook': string
'concurrent-shared': number
}
export const DefaultUseSharedCache: UseSharedCacheSchema = {
'example-key': 'example default value',
// Test keys (for dataRefactorTest window)
// TODO: remove after testing
'concurrent-shared': 0,
'test-hook-shared-1': 'default-shared-value',
'test-multi-hook': 'hook-3-shared'
}
/**
* Persist cache schema defining allowed keys and their value types
* This ensures type safety and prevents key conflicts
*/
export type RendererPersistCacheSchema = {
'example-key': string
// Test keys (for dataRefactorTest window)
// TODO: remove after testing
'example-1': string
'example-2': string
'example-3': string
'example-4': string
}
export const DefaultRendererPersistCache: RendererPersistCacheSchema = {
'example-key': 'example default value',
// Test keys (for dataRefactorTest window)
// TODO: remove after testing
'example-1': 'example default value',
'example-2': 'example default value',
'example-3': 'example default value',
'example-4': 'example default value'
}
/**
* Type-safe cache key
*/
export type RendererPersistCacheKey = keyof RendererPersistCacheSchema
export type UseCacheKey = keyof UseCacheSchema
export type UseSharedCacheKey = keyof UseSharedCacheSchema

View File

@@ -1,43 +0,0 @@
/**
* Cache types and interfaces for CacheService
*
* Supports three-layer caching architecture:
* 1. Memory cache (cross-component within renderer)
* 2. Shared cache (cross-window via IPC)
* 3. Persist cache (cross-window with localStorage persistence)
*/
/**
* Cache entry with optional TTL support
*/
export interface CacheEntry<T = any> {
value: T
expireAt?: number // Unix timestamp
}
/**
* Cache synchronization message for IPC communication
*/
export interface CacheSyncMessage {
type: 'shared' | 'persist'
key: string
value: any
ttl?: number
}
/**
* Batch cache synchronization message
*/
export interface CacheSyncBatchMessage {
type: 'shared' | 'persist'
entries: Array<{
key: string
value: any
ttl?: number
}>
}
/**
* Cache subscription callback
*/
export type CacheSubscriber = () => void

View File

@@ -1,18 +0,0 @@
import type { MinAppType, Topic, WebSearchStatus } from '@types'
import type { UpdateInfo } from 'builder-util-runtime'
export type CacheAppUpdateState = {
info: UpdateInfo | null
checking: boolean
downloading: boolean
downloaded: boolean
downloadProgress: number
available: boolean
}
export type CacheActiveSearches = Record<string, WebSearchStatus>
// For cache schema, we use any for complex types to avoid circular dependencies
// The actual type checking will be done at runtime by the cache system
export type CacheMinAppType = MinAppType
export type CacheTopic = Topic

View File

@@ -1,687 +0,0 @@
/**
* Auto-generated preferences configuration
* Generated at: 2025-09-16T03:17:03.354Z
*
* This file is automatically generated from classification.json
* To update this file, modify classification.json and run:
* node .claude/data-classify/scripts/generate-preferences.js
*
* === AUTO-GENERATED CONTENT START ===
*/
import { TRANSLATE_PROMPT } from '@shared/config/prompts'
import * as PreferenceTypes from '@shared/data/preference/preferenceTypes'
/* eslint @typescript-eslint/member-ordering: ["error", {
"interfaces": { "order": "alphabetically" },
"typeLiterals": { "order": "alphabetically" }
}] */
export interface PreferenceSchemas {
default: {
// redux/settings/enableDeveloperMode
'app.developer_mode.enabled': boolean
// redux/settings/disableHardwareAcceleration
'app.disable_hardware_acceleration': boolean
// redux/settings/autoCheckUpdate
'app.dist.auto_update.enabled': boolean
// redux/settings/testChannel
'app.dist.test_plan.channel': PreferenceTypes.UpgradeChannel
// redux/settings/testPlan
'app.dist.test_plan.enabled': boolean
// redux/settings/language
'app.language': PreferenceTypes.LanguageVarious | null
// redux/settings/launchOnBoot
'app.launch_on_boot': boolean
// redux/settings/notification.assistant
'app.notification.assistant.enabled': boolean
// redux/settings/notification.backup
'app.notification.backup.enabled': boolean
// redux/settings/notification.knowledge
'app.notification.knowledge.enabled': boolean
// redux/settings/enableDataCollection
'app.privacy.data_collection.enabled': boolean
// redux/settings/proxyBypassRules
'app.proxy.bypass_rules': string
// redux/settings/proxyMode
'app.proxy.mode': PreferenceTypes.ProxyMode
// redux/settings/proxyUrl
'app.proxy.url': string
// redux/settings/enableSpellCheck
'app.spell_check.enabled': boolean
// redux/settings/spellCheckLanguages
'app.spell_check.languages': string[]
// redux/settings/tray
'app.tray.enabled': boolean
// redux/settings/trayOnClose
'app.tray.on_close': boolean
// redux/settings/launchToTray
'app.tray.on_launch': boolean
// redux/settings/userId
'app.user.id': string
// redux/settings/userName
'app.user.name': string
// electronStore/ZoomFactor/ZoomFactor
'app.zoom_factor': number
// redux/settings/clickAssistantToShowTopic
'assistant.click_to_show_topic': boolean
// redux/settings/assistantIconType
'assistant.icon_type': PreferenceTypes.AssistantIconType
// redux/settings/showAssistants
'assistant.tab.show': boolean
// redux/settings/assistantsTabSortType
'assistant.tab.sort_type': PreferenceTypes.AssistantTabSortType
// redux/settings/codeCollapsible
'chat.code.collapsible': boolean
// redux/settings/codeEditor.autocompletion
'chat.code.editor.autocompletion': boolean
// redux/settings/codeEditor.enabled
'chat.code.editor.enabled': boolean
// redux/settings/codeEditor.foldGutter
'chat.code.editor.fold_gutter': boolean
// redux/settings/codeEditor.highlightActiveLine
'chat.code.editor.highlight_active_line': boolean
// redux/settings/codeEditor.keymap
'chat.code.editor.keymap': boolean
// redux/settings/codeEditor.themeDark
'chat.code.editor.theme_dark': string
// redux/settings/codeEditor.themeLight
'chat.code.editor.theme_light': string
// redux/settings/codeExecution.enabled
'chat.code.execution.enabled': boolean
// redux/settings/codeExecution.timeoutMinutes
'chat.code.execution.timeout_minutes': number
// redux/settings/codeFancyBlock
'chat.code.fancy_block': boolean
// redux/settings/codeImageTools
'chat.code.image_tools': boolean
// redux/settings/codePreview.themeDark
'chat.code.preview.theme_dark': string
// redux/settings/codePreview.themeLight
'chat.code.preview.theme_light': string
// redux/settings/codeShowLineNumbers
'chat.code.show_line_numbers': boolean
// redux/settings/codeViewer.themeDark
'chat.code.viewer.theme_dark': string
// redux/settings/codeViewer.themeLight
'chat.code.viewer.theme_light': string
// redux/settings/codeWrappable
'chat.code.wrappable': boolean
// redux/settings/pasteLongTextAsFile
'chat.input.paste_long_text_as_file': boolean
// redux/settings/pasteLongTextThreshold
'chat.input.paste_long_text_threshold': number
// redux/settings/enableQuickPanelTriggers
'chat.input.quick_panel.triggers_enabled': boolean
// redux/settings/sendMessageShortcut
'chat.input.send_message_shortcut': PreferenceTypes.SendMessageShortcut
// redux/settings/showInputEstimatedTokens
'chat.input.show_estimated_tokens': boolean
// redux/settings/autoTranslateWithSpace
'chat.input.translate.auto_translate_with_space': boolean
// redux/settings/showTranslateConfirm
'chat.input.translate.show_confirm': boolean
// redux/settings/confirmDeleteMessage
'chat.message.confirm_delete': boolean
// redux/settings/confirmRegenerateMessage
'chat.message.confirm_regenerate': boolean
// redux/settings/messageFont
'chat.message.font': string
// redux/settings/fontSize
'chat.message.font_size': number
// redux/settings/mathEngine
'chat.message.math.engine': PreferenceTypes.MathEngine
// redux/settings/mathEnableSingleDollar
'chat.message.math.single_dollar': boolean
// redux/settings/foldDisplayMode
'chat.message.multi_model.fold_display_mode': PreferenceTypes.MultiModelFoldDisplayMode
// redux/settings/gridColumns
'chat.message.multi_model.grid_columns': number
// redux/settings/gridPopoverTrigger
'chat.message.multi_model.grid_popover_trigger': PreferenceTypes.MultiModelGridPopoverTrigger
// redux/settings/multiModelMessageStyle
'chat.message.multi_model.style': PreferenceTypes.MultiModelMessageStyle
// redux/settings/messageNavigation
'chat.message.navigation_mode': PreferenceTypes.ChatMessageNavigationMode
// redux/settings/renderInputMessageAsMarkdown
'chat.message.render_as_markdown': boolean
// redux/settings/showMessageDivider
'chat.message.show_divider': boolean
// redux/settings/showMessageOutline
'chat.message.show_outline': boolean
// redux/settings/showPrompt
'chat.message.show_prompt': boolean
// redux/settings/messageStyle
'chat.message.style': PreferenceTypes.ChatMessageStyle
// redux/settings/thoughtAutoCollapse
'chat.message.thought.auto_collapse': boolean
// redux/settings/narrowMode
'chat.narrow_mode': boolean
// redux/settings/skipBackupFile
'data.backup.general.skip_backup_file': boolean
// redux/settings/localBackupAutoSync
'data.backup.local.auto_sync': boolean
// redux/settings/localBackupDir
'data.backup.local.dir': string
// redux/settings/localBackupMaxBackups
'data.backup.local.max_backups': number
// redux/settings/localBackupSkipBackupFile
'data.backup.local.skip_backup_file': boolean
// redux/settings/localBackupSyncInterval
'data.backup.local.sync_interval': number
// redux/nutstore/nutstoreAutoSync
'data.backup.nutstore.auto_sync': boolean
// redux/nutstore/nutstoreMaxBackups
'data.backup.nutstore.max_backups': number
// redux/nutstore/nutstorePath
'data.backup.nutstore.path': string
// redux/nutstore/nutstoreSkipBackupFile
'data.backup.nutstore.skip_backup_file': boolean
// redux/nutstore/nutstoreSyncInterval
'data.backup.nutstore.sync_interval': number
// redux/nutstore/nutstoreToken
'data.backup.nutstore.token': string
// redux/settings/s3.accessKeyId
'data.backup.s3.access_key_id': string
// redux/settings/s3.autoSync
'data.backup.s3.auto_sync': boolean
// redux/settings/s3.bucket
'data.backup.s3.bucket': string
// redux/settings/s3.endpoint
'data.backup.s3.endpoint': string
// redux/settings/s3.maxBackups
'data.backup.s3.max_backups': number
// redux/settings/s3.region
'data.backup.s3.region': string
// redux/settings/s3.root
'data.backup.s3.root': string
// redux/settings/s3.secretAccessKey
'data.backup.s3.secret_access_key': string
// redux/settings/s3.skipBackupFile
'data.backup.s3.skip_backup_file': boolean
// redux/settings/s3.syncInterval
'data.backup.s3.sync_interval': number
// redux/settings/webdavAutoSync
'data.backup.webdav.auto_sync': boolean
// redux/settings/webdavDisableStream
'data.backup.webdav.disable_stream': boolean
// redux/settings/webdavHost
'data.backup.webdav.host': string
// redux/settings/webdavMaxBackups
'data.backup.webdav.max_backups': number
// redux/settings/webdavPass
'data.backup.webdav.pass': string
// redux/settings/webdavPath
'data.backup.webdav.path': string
// redux/settings/webdavSkipBackupFile
'data.backup.webdav.skip_backup_file': boolean
// redux/settings/webdavSyncInterval
'data.backup.webdav.sync_interval': number
// redux/settings/webdavUser
'data.backup.webdav.user': string
// redux/settings/excludeCitationsInExport
'data.export.markdown.exclude_citations': boolean
// redux/settings/forceDollarMathInMarkdown
'data.export.markdown.force_dollar_math': boolean
// redux/settings/markdownExportPath
'data.export.markdown.path': string | null
// redux/settings/showModelNameInMarkdown
'data.export.markdown.show_model_name': boolean
// redux/settings/showModelProviderInMarkdown
'data.export.markdown.show_model_provider': boolean
// redux/settings/standardizeCitationsInExport
'data.export.markdown.standardize_citations': boolean
// redux/settings/useTopicNamingForMessageTitle
'data.export.markdown.use_topic_naming_for_message_title': boolean
// redux/settings/exportMenuOptions.docx
'data.export.menus.docx': boolean
// redux/settings/exportMenuOptions.image
'data.export.menus.image': boolean
// redux/settings/exportMenuOptions.joplin
'data.export.menus.joplin': boolean
// redux/settings/exportMenuOptions.markdown
'data.export.menus.markdown': boolean
// redux/settings/exportMenuOptions.markdown_reason
'data.export.menus.markdown_reason': boolean
// redux/settings/exportMenuOptions.notes
'data.export.menus.notes': boolean
// redux/settings/exportMenuOptions.notion
'data.export.menus.notion': boolean
// redux/settings/exportMenuOptions.obsidian
'data.export.menus.obsidian': boolean
// redux/settings/exportMenuOptions.plain_text
'data.export.menus.plain_text': boolean
// redux/settings/exportMenuOptions.siyuan
'data.export.menus.siyuan': boolean
// redux/settings/exportMenuOptions.yuque
'data.export.menus.yuque': boolean
// redux/settings/joplinExportReasoning
'data.integration.joplin.export_reasoning': boolean
// redux/settings/joplinToken
'data.integration.joplin.token': string
// redux/settings/joplinUrl
'data.integration.joplin.url': string
// redux/settings/notionApiKey
'data.integration.notion.api_key': string
// redux/settings/notionDatabaseID
'data.integration.notion.database_id': string
// redux/settings/notionExportReasoning
'data.integration.notion.export_reasoning': boolean
// redux/settings/notionPageNameKey
'data.integration.notion.page_name_key': string
// redux/settings/defaultObsidianVault
'data.integration.obsidian.default_vault': string
// redux/settings/siyuanApiUrl
'data.integration.siyuan.api_url': string | null
// redux/settings/siyuanBoxId
'data.integration.siyuan.box_id': string | null
// redux/settings/siyuanRootPath
'data.integration.siyuan.root_path': string | null
// redux/settings/siyuanToken
'data.integration.siyuan.token': string | null
// redux/settings/yuqueRepoId
'data.integration.yuque.repo_id': string
// redux/settings/yuqueToken
'data.integration.yuque.token': string
// redux/settings/yuqueUrl
'data.integration.yuque.url': string
// redux/settings/apiServer.apiKey
'feature.csaas.api_key': string
// redux/settings/apiServer.enabled
'feature.csaas.enabled': boolean
// redux/settings/apiServer.host
'feature.csaas.host': string
// redux/settings/apiServer.port
'feature.csaas.port': number
// redux/settings/maxKeepAliveMinapps
'feature.minapp.max_keep_alive': number
// redux/settings/minappsOpenLinkExternal
'feature.minapp.open_link_external': boolean
// redux/settings/showOpenedMinappsInSidebar
'feature.minapp.show_opened_in_sidebar': boolean
// redux/note/settings.defaultEditMode
'feature.notes.default_edit_mode': string
// redux/note/settings.defaultViewMode
'feature.notes.default_view_mode': string
// redux/note/settings.fontFamily
'feature.notes.font_family': string
// redux/note/settings.fontSize
'feature.notes.font_size': number
// redux/note/settings.isFullWidth
'feature.notes.full_width': boolean
// redux/note/notesPath
'feature.notes.path': string
// redux/note/settings.showTabStatus
'feature.notes.show_tab_status': boolean
// redux/note/settings.showTableOfContents
'feature.notes.show_table_of_contents': boolean
// redux/note/settings.showWorkspace
'feature.notes.show_workspace': boolean
// redux/note/sortType
'feature.notes.sort_type': string
// redux/settings/clickTrayToShowQuickAssistant
'feature.quick_assistant.click_tray_to_show': boolean
// redux/settings/enableQuickAssistant
'feature.quick_assistant.enabled': boolean
// redux/settings/readClipboardAtStartup
'feature.quick_assistant.read_clipboard_at_startup': boolean
// redux/selectionStore/actionItems
'feature.selection.action_items': PreferenceTypes.SelectionActionItem[]
// redux/selectionStore/actionWindowOpacity
'feature.selection.action_window_opacity': number
// redux/selectionStore/isAutoClose
'feature.selection.auto_close': boolean
// redux/selectionStore/isAutoPin
'feature.selection.auto_pin': boolean
// redux/selectionStore/isCompact
'feature.selection.compact': boolean
// redux/selectionStore/selectionEnabled
'feature.selection.enabled': boolean
// redux/selectionStore/filterList
'feature.selection.filter_list': string[]
// redux/selectionStore/filterMode
'feature.selection.filter_mode': PreferenceTypes.SelectionFilterMode
// redux/selectionStore/isFollowToolbar
'feature.selection.follow_toolbar': boolean
// redux/selectionStore/isRemeberWinSize
'feature.selection.remember_win_size': boolean
// redux/selectionStore/triggerMode
'feature.selection.trigger_mode': PreferenceTypes.SelectionTriggerMode
// redux/settings/translateModelPrompt
'feature.translate.model_prompt': string
// redux/settings/targetLanguage
'feature.translate.target_language': string
// redux/shortcuts/shortcuts.exit_fullscreen
'shortcut.app.exit_fullscreen': Record<string, unknown>
// redux/shortcuts/shortcuts.search_message
'shortcut.app.search_message': Record<string, unknown>
// redux/shortcuts/shortcuts.show_app
'shortcut.app.show_main_window': Record<string, unknown>
// redux/shortcuts/shortcuts.mini_window
'shortcut.app.show_mini_window': Record<string, unknown>
// redux/shortcuts/shortcuts.show_settings
'shortcut.app.show_settings': Record<string, unknown>
// redux/shortcuts/shortcuts.toggle_show_assistants
'shortcut.app.toggle_show_assistants': Record<string, unknown>
// redux/shortcuts/shortcuts.zoom_in
'shortcut.app.zoom_in': Record<string, unknown>
// redux/shortcuts/shortcuts.zoom_out
'shortcut.app.zoom_out': Record<string, unknown>
// redux/shortcuts/shortcuts.zoom_reset
'shortcut.app.zoom_reset': Record<string, unknown>
// redux/shortcuts/shortcuts.clear_topic
'shortcut.chat.clear': Record<string, unknown>
// redux/shortcuts/shortcuts.copy_last_message
'shortcut.chat.copy_last_message': Record<string, unknown>
// redux/shortcuts/shortcuts.search_message_in_chat
'shortcut.chat.search_message': Record<string, unknown>
// redux/shortcuts/shortcuts.toggle_new_context
'shortcut.chat.toggle_new_context': Record<string, unknown>
// redux/shortcuts/shortcuts.selection_assistant_select_text
'shortcut.selection.get_text': Record<string, unknown>
// redux/shortcuts/shortcuts.selection_assistant_toggle
'shortcut.selection.toggle_enabled': Record<string, unknown>
// redux/shortcuts/shortcuts.new_topic
'shortcut.topic.new': Record<string, unknown>
// redux/settings/enableTopicNaming
'topic.naming.enabled': boolean
// redux/settings/topicNamingPrompt
'topic.naming_prompt': string
// redux/settings/topicPosition
'topic.position': string
// redux/settings/pinTopicsToTop
'topic.tab.pin_to_top': boolean
// redux/settings/showTopics
'topic.tab.show': boolean
// redux/settings/showTopicTime
'topic.tab.show_time': boolean
// redux/settings/customCss
'ui.custom_css': string
// redux/settings/navbarPosition
'ui.navbar.position': 'left' | 'top'
// redux/settings/sidebarIcons.disabled
'ui.sidebar.icons.invisible': PreferenceTypes.SidebarIcon[]
// redux/settings/sidebarIcons.visible
'ui.sidebar.icons.visible': PreferenceTypes.SidebarIcon[]
// redux/settings/theme
'ui.theme_mode': PreferenceTypes.ThemeMode
// redux/settings/userTheme.userCodeFontFamily
'ui.theme_user.code_font_family': string
// redux/settings/userTheme.colorPrimary
'ui.theme_user.color_primary': string
// redux/settings/userTheme.userFontFamily
'ui.theme_user.font_family': string
// redux/settings/windowStyle
'ui.window_style': PreferenceTypes.WindowStyle
}
}
/* eslint sort-keys: ["error", "asc", {"caseSensitive": true, "natural": false}] */
export const DefaultPreferences: PreferenceSchemas = {
default: {
'app.developer_mode.enabled': false,
'app.disable_hardware_acceleration': false,
'app.dist.auto_update.enabled': true,
'app.dist.test_plan.channel': PreferenceTypes.UpgradeChannel.LATEST,
'app.dist.test_plan.enabled': false,
'app.language': null,
'app.launch_on_boot': false,
'app.notification.assistant.enabled': false,
'app.notification.backup.enabled': false,
'app.notification.knowledge.enabled': false,
'app.privacy.data_collection.enabled': false,
'app.proxy.bypass_rules': '',
'app.proxy.mode': 'system',
'app.proxy.url': '',
'app.spell_check.enabled': false,
'app.spell_check.languages': [],
'app.tray.enabled': true,
'app.tray.on_close': true,
'app.tray.on_launch': false,
'app.user.id': 'uuid()',
'app.user.name': '',
'app.zoom_factor': 1,
'assistant.click_to_show_topic': true,
'assistant.icon_type': 'emoji',
'assistant.tab.show': true,
'assistant.tab.sort_type': 'list',
'chat.code.collapsible': false,
'chat.code.editor.autocompletion': true,
'chat.code.editor.enabled': false,
'chat.code.editor.fold_gutter': false,
'chat.code.editor.highlight_active_line': false,
'chat.code.editor.keymap': false,
'chat.code.editor.theme_dark': 'auto',
'chat.code.editor.theme_light': 'auto',
'chat.code.execution.enabled': false,
'chat.code.execution.timeout_minutes': 1,
'chat.code.fancy_block': true,
'chat.code.image_tools': false,
'chat.code.preview.theme_dark': 'auto',
'chat.code.preview.theme_light': 'auto',
'chat.code.show_line_numbers': false,
'chat.code.viewer.theme_dark': 'auto',
'chat.code.viewer.theme_light': 'auto',
'chat.code.wrappable': false,
'chat.input.paste_long_text_as_file': false,
'chat.input.paste_long_text_threshold': 1500,
'chat.input.quick_panel.triggers_enabled': false,
'chat.input.send_message_shortcut': 'Enter',
'chat.input.show_estimated_tokens': false,
'chat.input.translate.auto_translate_with_space': false,
'chat.input.translate.show_confirm': true,
'chat.message.confirm_delete': true,
'chat.message.confirm_regenerate': true,
'chat.message.font': 'system',
'chat.message.font_size': 14,
'chat.message.math.engine': 'KaTeX',
'chat.message.math.single_dollar': true,
'chat.message.multi_model.fold_display_mode': 'expanded',
'chat.message.multi_model.grid_columns': 2,
'chat.message.multi_model.grid_popover_trigger': 'click',
'chat.message.multi_model.style': 'horizontal',
'chat.message.navigation_mode': 'none',
'chat.message.render_as_markdown': false,
'chat.message.show_divider': true,
'chat.message.show_outline': false,
'chat.message.show_prompt': true,
'chat.message.style': 'plain',
'chat.message.thought.auto_collapse': true,
'chat.narrow_mode': false,
'data.backup.general.skip_backup_file': false,
'data.backup.local.auto_sync': false,
'data.backup.local.dir': '',
'data.backup.local.max_backups': 0,
'data.backup.local.skip_backup_file': false,
'data.backup.local.sync_interval': 0,
'data.backup.nutstore.auto_sync': false,
'data.backup.nutstore.max_backups': 0,
'data.backup.nutstore.path': '/cherry-studio',
'data.backup.nutstore.skip_backup_file': false,
'data.backup.nutstore.sync_interval': 0,
'data.backup.nutstore.token': '',
'data.backup.s3.access_key_id': '',
'data.backup.s3.auto_sync': false,
'data.backup.s3.bucket': '',
'data.backup.s3.endpoint': '',
'data.backup.s3.max_backups': 0,
'data.backup.s3.region': '',
'data.backup.s3.root': '',
'data.backup.s3.secret_access_key': '',
'data.backup.s3.skip_backup_file': false,
'data.backup.s3.sync_interval': 0,
'data.backup.webdav.auto_sync': false,
'data.backup.webdav.disable_stream': false,
'data.backup.webdav.host': '',
'data.backup.webdav.max_backups': 0,
'data.backup.webdav.pass': '',
'data.backup.webdav.path': '/cherry-studio',
'data.backup.webdav.skip_backup_file': false,
'data.backup.webdav.sync_interval': 0,
'data.backup.webdav.user': '',
'data.export.markdown.exclude_citations': false,
'data.export.markdown.force_dollar_math': false,
'data.export.markdown.path': null,
'data.export.markdown.show_model_name': false,
'data.export.markdown.show_model_provider': false,
'data.export.markdown.standardize_citations': false,
'data.export.markdown.use_topic_naming_for_message_title': false,
'data.export.menus.docx': true,
'data.export.menus.image': true,
'data.export.menus.joplin': true,
'data.export.menus.markdown': true,
'data.export.menus.markdown_reason': true,
'data.export.menus.notes': true,
'data.export.menus.notion': true,
'data.export.menus.obsidian': true,
'data.export.menus.plain_text': true,
'data.export.menus.siyuan': true,
'data.export.menus.yuque': true,
'data.integration.joplin.export_reasoning': false,
'data.integration.joplin.token': '',
'data.integration.joplin.url': '',
'data.integration.notion.api_key': '',
'data.integration.notion.database_id': '',
'data.integration.notion.export_reasoning': false,
'data.integration.notion.page_name_key': 'Name',
'data.integration.obsidian.default_vault': '',
'data.integration.siyuan.api_url': null,
'data.integration.siyuan.box_id': null,
'data.integration.siyuan.root_path': null,
'data.integration.siyuan.token': null,
'data.integration.yuque.repo_id': '',
'data.integration.yuque.token': '',
'data.integration.yuque.url': '',
'feature.csaas.api_key': '`cs-sk-${uuid()}`',
'feature.csaas.enabled': false,
'feature.csaas.host': 'localhost',
'feature.csaas.port': 23333,
'feature.minapp.max_keep_alive': 3,
'feature.minapp.open_link_external': false,
'feature.minapp.show_opened_in_sidebar': true,
'feature.notes.default_edit_mode': 'preview',
'feature.notes.default_view_mode': 'edit',
'feature.notes.font_family': 'default',
'feature.notes.font_size': 16,
'feature.notes.full_width': true,
'feature.notes.path': '',
'feature.notes.show_tab_status': true,
'feature.notes.show_table_of_contents': true,
'feature.notes.show_workspace': true,
'feature.notes.sort_type': 'sort_a2z',
'feature.quick_assistant.click_tray_to_show': false,
'feature.quick_assistant.enabled': false,
'feature.quick_assistant.read_clipboard_at_startup': true,
'feature.selection.action_items': [
{
enabled: true,
icon: 'languages',
id: 'translate',
isBuiltIn: true,
name: 'selection.action.builtin.translate'
},
{
enabled: true,
icon: 'file-question',
id: 'explain',
isBuiltIn: true,
name: 'selection.action.builtin.explain'
},
{ enabled: true, icon: 'scan-text', id: 'summary', isBuiltIn: true, name: 'selection.action.builtin.summary' },
{
enabled: true,
icon: 'search',
id: 'search',
isBuiltIn: true,
name: 'selection.action.builtin.search',
searchEngine: 'Google|https://www.google.com/search?q={{queryString}}'
},
{ enabled: true, icon: 'clipboard-copy', id: 'copy', isBuiltIn: true, name: 'selection.action.builtin.copy' },
{ enabled: false, icon: 'wand-sparkles', id: 'refine', isBuiltIn: true, name: 'selection.action.builtin.refine' },
{ enabled: false, icon: 'quote', id: 'quote', isBuiltIn: true, name: 'selection.action.builtin.quote' }
],
'feature.selection.action_window_opacity': 100,
'feature.selection.auto_close': false,
'feature.selection.auto_pin': false,
'feature.selection.compact': false,
'feature.selection.enabled': false,
'feature.selection.filter_list': [],
'feature.selection.filter_mode': PreferenceTypes.SelectionFilterMode.Default,
'feature.selection.follow_toolbar': true,
'feature.selection.remember_win_size': false,
'feature.selection.trigger_mode': PreferenceTypes.SelectionTriggerMode.Selected,
'feature.translate.model_prompt': TRANSLATE_PROMPT,
'feature.translate.target_language': 'en-us',
'shortcut.app.exit_fullscreen': { editable: false, enabled: true, key: ['Escape'], system: true },
'shortcut.app.search_message': {
editable: true,
enabled: true,
key: ['CommandOrControl', 'Shift', 'F'],
system: false
},
'shortcut.app.show_main_window': { editable: true, enabled: true, key: [], system: true },
'shortcut.app.show_mini_window': { editable: true, enabled: false, key: ['CommandOrControl', 'E'], system: true },
'shortcut.app.show_settings': { editable: false, enabled: true, key: ['CommandOrControl', ','], system: true },
'shortcut.app.toggle_show_assistants': {
editable: true,
enabled: true,
key: ['CommandOrControl', '['],
system: false
},
'shortcut.app.zoom_in': { editable: false, enabled: true, key: ['CommandOrControl', '='], system: true },
'shortcut.app.zoom_out': { editable: false, enabled: true, key: ['CommandOrControl', '-'], system: true },
'shortcut.app.zoom_reset': { editable: false, enabled: true, key: ['CommandOrControl', '0'], system: true },
'shortcut.chat.clear': { editable: true, enabled: true, key: ['CommandOrControl', 'L'], system: false },
'shortcut.chat.copy_last_message': {
editable: true,
enabled: false,
key: ['CommandOrControl', 'Shift', 'C'],
system: false
},
'shortcut.chat.search_message': { editable: true, enabled: true, key: ['CommandOrControl', 'F'], system: false },
'shortcut.chat.toggle_new_context': {
editable: true,
enabled: true,
key: ['CommandOrControl', 'K'],
system: false
},
'shortcut.selection.get_text': { editable: true, enabled: false, key: [], system: true },
'shortcut.selection.toggle_enabled': { editable: true, enabled: false, key: [], system: true },
'shortcut.topic.new': { editable: true, enabled: true, key: ['CommandOrControl', 'N'], system: false },
'topic.naming.enabled': true,
'topic.naming_prompt': '',
'topic.position': 'left',
'topic.tab.pin_to_top': false,
'topic.tab.show': true,
'topic.tab.show_time': false,
'ui.custom_css': '',
'ui.navbar.position': 'top',
'ui.sidebar.icons.invisible': [],
'ui.sidebar.icons.visible': [
'assistants',
'store',
'paintings',
'translate',
'minapp',
'knowledge',
'files',
'code_tools',
'notes'
],
'ui.theme_mode': PreferenceTypes.ThemeMode.system,
'ui.theme_user.code_font_family': '',
'ui.theme_user.color_primary': '#00b96b',
'ui.theme_user.font_family': '',
'ui.window_style': 'opaque'
}
}
// === AUTO-GENERATED CONTENT END ===
/**
* 生成统计:
* - 总配置项: 197
* - electronStore项: 1
* - redux项: 196
* - localStorage项: 0
*/

View File

@@ -1,97 +0,0 @@
import type { PreferenceSchemas } from './preferenceSchemas'
export type PreferenceDefaultScopeType = PreferenceSchemas['default']
export type PreferenceKeyType = keyof PreferenceDefaultScopeType
export type PreferenceUpdateOptions = {
optimistic: boolean
}
export type PreferenceShortcutType = {
key: string[]
editable: boolean
enabled: boolean
system: boolean
}
export enum SelectionTriggerMode {
Selected = 'selected',
Ctrlkey = 'ctrlkey',
Shortcut = 'shortcut'
}
export enum SelectionFilterMode {
Default = 'default',
Whitelist = 'whitelist',
Blacklist = 'blacklist'
}
export type SelectionActionItem = {
id: string
name: string
enabled: boolean
isBuiltIn: boolean
icon?: string
prompt?: string
assistantId?: string
selectedText?: string
searchEngine?: string
}
export enum ThemeMode {
light = 'light',
dark = 'dark',
system = 'system'
}
/** 有限的UI语言 */
export type LanguageVarious =
| 'zh-CN'
| 'zh-TW'
| 'el-GR'
| 'en-US'
| 'es-ES'
| 'fr-FR'
| 'ja-JP'
| 'pt-PT'
| 'ru-RU'
| 'de-DE'
export type WindowStyle = 'transparent' | 'opaque'
export type SendMessageShortcut = 'Enter' | 'Shift+Enter' | 'Ctrl+Enter' | 'Command+Enter' | 'Alt+Enter'
export type AssistantTabSortType = 'tags' | 'list'
export type SidebarIcon =
| 'assistants'
| 'store'
| 'paintings'
| 'translate'
| 'minapp'
| 'knowledge'
| 'files'
| 'code_tools'
| 'notes'
export type AssistantIconType = 'model' | 'emoji' | 'none'
export type ProxyMode = 'system' | 'custom' | 'none'
export type MultiModelFoldDisplayMode = 'expanded' | 'compact'
export type MathEngine = 'KaTeX' | 'MathJax' | 'none'
export enum UpgradeChannel {
LATEST = 'latest', // 最新稳定版本
RC = 'rc', // 公测版本
BETA = 'beta' // 预览版本
}
export type ChatMessageStyle = 'plain' | 'bubble'
export type ChatMessageNavigationMode = 'none' | 'buttons' | 'anchor'
export type MultiModelMessageStyle = 'horizontal' | 'vertical' | 'fold' | 'grid'
export type MultiModelGridPopoverTrigger = 'hover' | 'click'

View File

@@ -1,15 +0,0 @@
node_modules/
dist/
*.log
.DS_Store
# Storybook build output
storybook-static/
# IDE
.vscode/
.idea/
# Temporary files
*.tmp
*.temp

View File

@@ -1,17 +0,0 @@
import type { StorybookConfig } from '@storybook/react-vite'
const config: StorybookConfig = {
stories: ['../stories/components/**/*.stories.@(js|jsx|ts|tsx)'],
addons: ['@storybook/addon-docs', '@storybook/addon-themes'],
framework: '@storybook/react-vite',
viteFinal: async (config) => {
const { mergeConfig } = await import('vite')
// 动态导入 @tailwindcss/vite 以避免 ESM/CJS 兼容性问题
const tailwindPlugin = (await import('@tailwindcss/vite')).default
return mergeConfig(config, {
plugins: [tailwindPlugin()]
})
}
}
export default config

View File

@@ -1,18 +0,0 @@
import '../stories/tailwind.css'
import { withThemeByClassName } from '@storybook/addon-themes'
import type { Preview } from '@storybook/react'
const preview: Preview = {
decorators: [
withThemeByClassName({
themes: {
light: '',
dark: 'dark'
},
defaultTheme: 'light'
})
]
}
export default preview

View File

@@ -1,150 +0,0 @@
# Cherry Studio UI Migration Plan
## Overview
This document outlines the detailed plan for migrating Cherry Studio from antd + styled-components to shadcn/ui + Tailwind CSS. We will adopt a progressive migration strategy to ensure system stability and development efficiency, while gradually implementing UI refactoring in collaboration with UI designers.
## Migration Strategy
### Target Tech Stack
- **UI Component Library**: shadcn/ui (replacing antd and previously migrated HeroUI)
- **Styling Solution**: Tailwind CSS v4 (replacing styled-components)
- **Design System**: Custom CSS variable system (`--cs-*` namespace)
- **Theme System**: CSS variables + Tailwind CSS theme
### Migration Principles
1. **Backward Compatibility**: Old components continue working until new components are fully available
2. **Progressive Migration**: Migrate components one by one to avoid large-scale rewrites
3. **Feature Parity**: Ensure new components have all the functionality of old components
4. **Design Consistency**: Follow new design system specifications (see [README.md](./README.md))
5. **Performance Priority**: Optimize bundle size and rendering performance
6. **Designer Collaboration**: Work with UI designers for gradual component encapsulation and UI optimization
## Usage Example
```typescript
// Import components from @cherrystudio/ui
import { Spinner, DividerWithText, InfoTooltip } from '@cherrystudio/ui'
// Use in components
function MyComponent() {
return (
<div>
<Spinner size={24} />
<DividerWithText text="Divider Text" />
<InfoTooltip content="Tooltip message" />
</div>
)
}
```
## Directory Structure
```text
@packages/ui/
├── src/
│ ├── components/ # Main components directory
│ │ ├── primitives/ # Basic/primitive components (Avatar, ErrorBoundary, Selector, etc.)
│ │ │ └── shadcn-io/ # shadcn/ui components (dropzone, etc.)
│ │ ├── icons/ # Icon components (Icon, FileIcons, etc.)
│ │ └── composites/ # Composite components (CodeEditor, ListItem, etc.)
│ ├── hooks/ # Custom React Hooks
│ ├── styles/ # Global styles and CSS variables
│ ├── types/ # TypeScript type definitions
│ ├── utils/ # Utility functions
│ └── index.ts # Main export file
```
### Component Classification Guide
When submitting PRs, please place components in the correct directory based on their function:
- **primitives**: Basic and primitive UI elements, shadcn/ui components
- `Avatar`: Avatar components
- `ErrorBoundary`: Error boundary components
- `Selector`: Selection components
- `shadcn-io/`: Direct shadcn/ui components or adaptations
- **icons**: All icon-related components
- `Icon`: Icon factory and basic icons
- `FileIcons`: File-specific icons
- Loading/spinner icons (SvgSpinners180Ring, ToolsCallingIcon, etc.)
- **composites**: Complex components made from multiple primitives
- `CodeEditor`: Code editing components
- `ListItem`: List item components
- `ThinkingEffect`: Animation components
- Form and interaction components (DraggableList, EditableNumber, etc.)
## Component Extraction Criteria
### Extraction Standards
1. **Usage Frequency**: Component is used in ≥ 3 places in the codebase
2. **Future Reusability**: Expected to be used in multiple scenarios in the future
3. **Business Complexity**: Component contains complex interaction logic or state management
4. **Maintenance Cost**: Centralized management can reduce maintenance overhead
5. **Design Consistency**: Components that require unified visual and interaction experience
6. **Test Coverage**: As common components, they facilitate unit test writing and maintenance
### Extraction Principles
- **Single Responsibility**: Each component should only handle one clear function
- **Highly Configurable**: Provide flexible configuration options through props
- **Backward Compatible**: New versions maintain API backward compatibility
- **Complete Documentation**: Provide clear API documentation and usage examples
- **Type Safety**: Use TypeScript to ensure type safety
### Cases Not Recommended for Extraction
- Simple display components used only on a single page
- Overly customized business logic components
- Components tightly coupled to specific data sources
## Migration Steps
| Phase | Status | Main Tasks | Description |
| --- | --- | --- | --- |
| **Phase 1** | ✅ **Completed** | **Design System Integration** | • Converted design tokens from todocss.css to tokens.css with `--cs-*` namespace<br>• Created theme.css mapping all design tokens to standard Tailwind classes<br>• Extended Tailwind with semantic spacing (5xs~8xl) and radius (4xs~3xl) systems<br>• Established two usage modes: full override and selective override<br>• Cleaned up main package's conflicting Shadcn theme definitions |
| **Phase 2** | ⏳ **To Start** | **Component Migration and Optimization** | • Filter components for migration based on extraction criteria<br>• Remove antd dependencies, replace with shadcn/ui<br>• Remove HeroUI dependencies, replace with shadcn/ui<br>• Remove styled-components, replace with Tailwind CSS + design system variables<br>• Optimize component APIs and type definitions |
| **Phase 3** | ⏳ **To Start** | **UI Refactoring and Optimization** | • Gradually implement UI refactoring with UI designers<br>• Ensure visual consistency and user experience<br>• Performance optimization and code quality improvement |
## Notes
1. **Do NOT migrate** components with these dependencies (can be migrated after decoupling):
- window.api calls
- Redux (useSelector, useDispatch, etc.)
- Other external data sources
2. **Can migrate** but need decoupling later:
- Components using i18n (change i18n to props)
- Components using antd (replace with shadcn/ui later)
- Components using HeroUI (replace with shadcn/ui later)
3. **Submission Guidelines**:
- Each PR should focus on one category of components
- Ensure all migrated components are exported
- Follow component extraction criteria, only migrate qualified components
## Design System Integration
### CSS Variable System
- All design tokens use `--cs-*` namespace (e.g., `--cs-primary`, `--cs-red-500`)
- Complete color palette: 17 colors × 11 shades each
- Semantic spacing system: `5xs` through `8xl` (16 levels)
- Semantic radius system: `4xs` through `3xl` plus `round` (11 levels)
- Full light/dark mode support
- See [README.md](./README.md) for usage documentation
### Migration Priority Adjustment
1. **High Priority**: Basic components (buttons, inputs, tags, etc.)
2. **Medium Priority**: Display components (cards, lists, tables, etc.)
3. **Low Priority**: Composite components and business-coupled components
### UI Designer Collaboration
- All component designs need confirmation from UI designers
- Gradually implement UI refactoring to maintain visual consistency
- New components must comply with design system specifications

View File

@@ -1,263 +0,0 @@
# @cherrystudio/ui
Cherry Studio UI 组件库 - 为 Cherry Studio 设计的 React 组件集合
## ✨ 特性
- 🎨 **设计系统**: 完整的 CherryStudio 设计令牌17种颜色 × 11个色阶 + 语义化主题)
- 🌓 **Dark Mode**: 开箱即用的深色模式支持
- 🚀 **Tailwind v4**: 基于最新 Tailwind CSS v4 构建
- 📦 **灵活导入**: 2种样式导入方式满足不同使用场景
- 🔷 **TypeScript**: 完整的类型定义和智能提示
- 🎯 **零冲突**: CSS 变量隔离,不覆盖用户主题
---
## 🚀 快速开始
### 安装
```bash
npm install @cherrystudio/ui
# peer dependencies
npm install @heroui/react framer-motion react react-dom tailwindcss
```
### 两种使用方式
#### 方式 1完整覆盖 ✨
使用完整的 CherryStudio 设计系统,所有 Tailwind 类名映射到设计系统。
```css
/* app.css */
@import '@cherrystudio/ui/styles/theme.css';
```
**特点**
- ✅ 直接使用标准 Tailwind 类名(`bg-primary``bg-red-500``p-md``rounded-lg`
- ✅ 所有颜色使用设计师定义的值
- ✅ 扩展的 Spacing 系统(`p-5xs` ~ `p-8xl`,共 16 个语义化尺寸)
- ✅ 扩展的 Radius 系统(`rounded-4xs` ~ `rounded-3xl`,共 11 个圆角)
- ⚠️ 会完全覆盖 Tailwind 默认主题
**示例**
```tsx
<Button className="bg-primary text-red-500 p-md rounded-lg">
{/* bg-primary → 品牌色lime-500 */}
{/* text-red-500 → 设计师定义的红色 */}
{/* p-md → 2.5remspacing-md */}
{/* rounded-lg → 2.5remradius-lg */}
</Button>
{/* 扩展的工具类 */}
<div className="p-5xs"> (0.5rem)</div>
<div className="p-xs"> (1rem)</div>
<div className="p-sm"> (1.5rem)</div>
<div className="p-md"> (2.5rem)</div>
<div className="p-lg"> (3.5rem)</div>
<div className="p-xl"> (5rem)</div>
<div className="p-8xl"> (15rem)</div>
<div className="rounded-4xs"> (0.25rem)</div>
<div className="rounded-xs"> (1rem)</div>
<div className="rounded-md"> (2rem)</div>
<div className="rounded-xl"> (3rem)</div>
<div className="rounded-round"> (999px)</div>
```
#### 方式 2选择性覆盖 🎯
只导入设计令牌CSS 变量),手动选择要覆盖的部分。
```css
/* app.css */
@import 'tailwindcss';
@import '@cherrystudio/ui/styles/tokens.css';
/* 只使用部分设计系统 */
@theme {
--color-primary: var(--cs-primary); /* 使用 CS 的主色 */
--color-red-500: oklch(...); /* 使用自己的红色 */
--spacing-md: var(--cs-size-md); /* 使用 CS 的间距 */
--radius-lg: 1rem; /* 使用自己的圆角 */
}
```
**特点**
- ✅ 不覆盖任何 Tailwind 默认主题
- ✅ 通过 CSS 变量访问所有设计令牌(`var(--cs-primary)``var(--cs-red-500)`
- ✅ 精细控制哪些使用 CS、哪些保持原样
- ✅ 适合有自己设计系统但想借用部分 CS 设计令牌的场景
**示例**
```tsx
{/* 通过 CSS 变量使用 CS 设计令牌 */}
<button style={{ backgroundColor: 'var(--cs-primary)' }}>
使 CherryStudio
</button>
{/* 保持原有的 Tailwind 类名不受影响 */}
<div className="bg-red-500">
使 Tailwind
</div>
{/* 可用的 CSS 变量 */}
<div style={{
color: 'var(--cs-primary)', // 品牌色
backgroundColor: 'var(--cs-red-500)', // 红色-500
padding: 'var(--cs-size-md)', // 间距
borderRadius: 'var(--cs-radius-lg)' // 圆角
}} />
```
### Provider 配置
在你的 App 根组件中添加 HeroUI Provider
```tsx
import { HeroUIProvider } from '@heroui/react'
function App() {
return (
<HeroUIProvider>
{/* 你的应用内容 */}
</HeroUIProvider>
)
}
```
## 使用
### 基础组件
```tsx
import { Button, Input } from '@cherrystudio/ui'
function App() {
return (
<div>
<Button variant="primary" size="md">
</Button>
<Input
type="text"
placeholder="请输入内容"
onChange={(value) => console.log(value)}
/>
</div>
)
}
```
### 分模块导入
```tsx
// 只导入组件
import { Button } from '@cherrystudio/ui/components'
// 只导入工具函数
import { cn, formatFileSize } from '@cherrystudio/ui/utils'
```
## 开发
```bash
# 安装依赖
yarn install
# 开发模式(监听文件变化)
yarn dev
# 构建
yarn build
# 类型检查
yarn type-check
# 运行测试
yarn test
```
## 目录结构
```text
src/
├── components/ # React 组件
│ ├── Button/ # 按钮组件
│ ├── Input/ # 输入框组件
│ └── index.ts # 组件导出
├── hooks/ # React Hooks
├── utils/ # 工具函数
├── types/ # 类型定义
└── index.ts # 主入口文件
```
## 组件列表
### Button 按钮
支持多种变体和尺寸的按钮组件。
**Props:**
- `variant`: 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger'
- `size`: 'sm' | 'md' | 'lg'
- `loading`: boolean
- `fullWidth`: boolean
- `leftIcon` / `rightIcon`: React.ReactNode
### Input 输入框
带有错误处理和密码显示切换的输入框组件。
**Props:**
- `type`: 'text' | 'password' | 'email' | 'number'
- `error`: boolean
- `errorMessage`: string
- `onChange`: (value: string) => void
## Hooks
### useDebounce
防抖处理,延迟执行状态更新。
### useLocalStorage
本地存储的 React Hook 封装。
### useClickOutside
检测点击元素外部区域。
### useCopyToClipboard
复制文本到剪贴板。
## 工具函数
### cn(...inputs)
基于 clsx 的类名合并工具,支持条件类名。
### formatFileSize(bytes)
格式化文件大小显示。
### debounce(func, delay)
防抖函数。
### throttle(func, delay)
节流函数。
## 许可证
MIT

View File

@@ -1,21 +0,0 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"aliases": {
"components": "@cherrystudio/ui/components",
"hooks": "@cherrystudio/ui/hooks",
"lib": "@cherrystudio/ui/lib",
"ui": "@cherrystudio/ui/components/primitives",
"utils": "@cherrystudio/ui/utils"
},
"iconLibrary": "lucide",
"rsc": false,
"style": "new-york",
"tailwind": {
"baseColor": "zinc",
"config": "",
"css": "src/styles/theme.css",
"cssVariables": true,
"prefix": ""
},
"tsx": true
}

View File

@@ -1,214 +0,0 @@
# todocss.css → design-tokens.css 转换日志
## ✅ 已转换的变量
### 基础颜色 (Primitive Colors)
- ✅ Neutral (50-950)
- ✅ Zinc (50-950)
- ✅ Red (50-950)
- ✅ Orange (50-950)
- ✅ Amber (50-950)
- ✅ Yellow (50-950)
- ✅ Lime (50-950) - 品牌主色
- ✅ Green (50-950)
- ✅ Emerald (50-950)
- ✅ Purple (50-950)
- ✅ Blue (50-950)
- ✅ Black & White
### 语义化颜色 (Semantic Colors)
-`--cs-primary` (Lime 500)
-`--cs-destructive` (Red 500)
-`--cs-success` (Green 500)
-`--cs-warning` (Amber 500)
-`--cs-background` (Zinc 50/900)
-`--cs-foreground` 系列 (main, secondary, muted)
-`--cs-border` 系列 (default, hover, active)
-`--cs-ring` (Focus)
### 容器颜色
-`--cs-card` (White/Black)
-`--cs-popover` (White/Black)
-`--cs-sidebar` (White/Black)
### UI 元素细分颜色 (新增补充)
-**Modal / Overlay**
- `--cs-modal-backdrop`
- `--cs-modal-thumb`
- `--cs-modal-thumb-hover`
-**Icon**
- `--cs-icon-default`
- `--cs-icon-hover`
-**Input / Select**
- `--cs-input-background`
- `--cs-input-border`
- `--cs-input-border-hover`
- `--cs-input-border-focus`
-**Primary Button**
- `--cs-primary-button-background`
- `--cs-primary-button-text`
- `--cs-primary-button-background-hover`
- `--cs-primary-button-background-active`
- `--cs-primary-button-background-2nd`
- `--cs-primary-button-background-3rd`
-**Secondary Button**
- `--cs-secondary-button-background`
- `--cs-secondary-button-text`
- `--cs-secondary-button-background-hover`
- `--cs-secondary-button-background-active`
- `--cs-secondary-button-border`
-**Ghost Button**
- `--cs-ghost-button-background`
- `--cs-ghost-button-text`
- `--cs-ghost-button-background-hover`
- `--cs-ghost-button-background-active`
### 尺寸系统
- ✅ Spacing/Sizing 合并为 `--cs-size-*` (5xs ~ 8xl)
- ✅ Border Radius (4xs ~ 3xl, round)
- ✅ Border Width (sm, md, lg)
### 字体排版
- ✅ Font Families (Heading, Body)
- ✅ Font Weights (修正单位错误: 400px → 400)
- ✅ Font Sizes (Body & Heading)
- ✅ Line Heights (Body & Heading)
- ✅ Paragraph Spacing
---
## ❌ 已废弃的变量
### Opacity 变量 (全部废弃)
使用 Tailwind 的 `/modifier` 语法替代:
| todocss.css | 替代方案 |
|-------------|---------|
| `--Opacity--Red--Red-80` | `bg-cs-destructive/80` |
| `--Opacity--Green--Green-60` | `bg-cs-success/60` |
| `--Opacity--White--White-10` | `bg-white/10` |
**原因**: Tailwind v4 原生支持透明度修饰符,无需单独定义变量。
---
## 🔧 关键修正
### 1. 单位错误
```css
/* ❌ todocss.css */
--Font_weight--Regular: 400px;
/* ✅ design-tokens.css */
--cs-font-weight-regular: 400;
```
### 2. px → rem 转换
```css
/* ❌ todocss.css */
--Spacing--md: 40px;
/* ✅ design-tokens.css */
--cs-size-md: 2.5rem; /* 40px / 16 = 2.5rem */
```
### 3. 变量合并
```css
/* ❌ todocss.css (冗余) */
--Spacing--md: 40px;
--Sizing--md: 40px;
/* ✅ design-tokens.css (合并) */
--cs-size-md: 2.5rem;
```
### 4. Dark Mode 分离
```css
/* ❌ todocss.css (Light 和 Dark 都在 :root) */
:root {
--Brand--Semantic_Colors--Background: var(--Primitive--Zinc--50);
--Brand--Semantic_Colors--Background: var(--Primitive--Zinc--900); /* 后面覆盖 */
}
/* ✅ design-tokens.css (正确分离) */
:root {
--cs-background: var(--cs-zinc-50);
}
.dark {
--cs-background: var(--cs-zinc-900);
}
```
---
## 📊 变量统计
| 分类 | todocss.css | design-tokens.css | 说明 |
|------|-------------|-------------------|------|
| Primitive Colors | ~250 | ~250 | 完整保留 |
| Semantic Colors | ~20 | ~20 | 完整转换 |
| UI Element Colors | ~30 | ~30 | ✅ 已补充完整 |
| Opacity Variables | ~50 | 0 | 废弃,用 `/modifier` |
| Spacing/Sizing | 32 | 16 | 合并去重 |
| Typography | ~50 | ~50 | 修正单位 |
| **总计** | ~430 | ~390 | 优化 40 个变量 |
---
## 🎨 Dark Mode 变量对比
| Light Mode | Dark Mode | 变量名 |
|-----------|-----------|-------|
| Zinc 50 | Zinc 900 | `--cs-background` |
| Black 90% | White 90% | `--cs-foreground` |
| Black 60% | White 60% | `--cs-foreground-secondary` |
| Black 10% | White 10% | `--cs-border` |
| White | Black | `--cs-card` |
| White | Black | `--cs-popover` |
| White | Black | `--cs-sidebar` |
| White | Black | `--cs-input-background` |
| Black 40% | Black 6% | `--cs-modal-backdrop` |
| Black 20% | White 20% | `--cs-modal-thumb` |
| Black 5% | White 10% | `--cs-secondary` |
| Black 0% | White 0% | `--cs-ghost-button-background` |
---
## ✅ 验证清单
- [x] 所有 Primitive 颜色已转换
- [x] 所有语义化颜色已转换
- [x] 所有 UI 元素颜色已转换
- [x] Dark Mode 变量完整
- [x] 尺寸单位统一为 rem
- [x] Font Weight 单位已修正
- [x] Opacity 变量已废弃
- [x] Spacing/Sizing 已合并
---
## 📝 使用指南
### 如果设计师更新 todocss.css
1. 对比此文档,找出新增/修改的变量
2. 按照转换规则更新 `design-tokens.css`
3. 验证 Light/Dark Mode 是否完整
4. 更新此日志
### 验证转换正确性
```bash
# 检查 Light Mode 变量数量
grep -c "^ --cs-" packages/ui/src/styles/design-tokens.css
# 检查 Dark Mode 覆盖数量
grep -c "^ --cs-" packages/ui/src/styles/design-tokens.css | grep -A 100 ".dark"
```

View File

@@ -1,26 +0,0 @@
# Design Reference
本文件夹包含设计师提供的原始设计令牌文件,仅作为参考使用。
## 文件说明
### todocss.css
- **来源**:设计师提供的原始设计令牌
- **状态**:已转换为 `src/styles/design-tokens.css`
- **用途**
- 追溯设计决策
- 验证转换正确性
- 设计师更新时作为对比基准
## 转换规则
原始文件 → 生产文件的转换规则参见:
- [DESIGN_SYSTEM.md](../DESIGN_SYSTEM.md)
- [USAGE_GUIDE.md](../USAGE_GUIDE.md)
## 注意事项
⚠️ **请勿直接使用此文件夹中的文件**
- 这些文件仅供参考
- 实际使用请导入 `src/styles/` 中的文件

View File

@@ -1,870 +0,0 @@
:root {
/* Typography: Desktop mode */
--Font_family--Heading: Inter;
--Font_weight--Regular: 400px;
--Font_size--Heading--2xl: 60px;
--Font_size--Heading--xl: 48px;
--Font_size--Heading--lg: 40px;
--Font_size--Heading--md: 32px;
--Font_size--Heading--sm: 24px;
--Font_size--Heading--xs: 20px;
--Line_height--Heading--xl: 80px;
--Line_height--Body--lg: 28px;
--Line_height--Body--md: 24px;
--Line_height--Body--sm: 24px;
--Line_height--Body--xs: 20px;
--Paragraph_spacing--Body--lg: 18px;
--Paragraph_spacing--Body--md: 16px;
--Paragraph_spacing--Body--sm: 14px;
--Paragraph_spacing--Body--xs: 12px;
--Line_height--Heading--lg: 60px;
--Line_height--Heading--md: 48px;
--Line_height--Heading--sm: 40px;
--Line_height--Heading--xs: 32px;
--Font_size--Body--lg: 18px;
--Font_size--Body--md: 16px;
--Font_size--Body--sm: 14px;
--Font_size--Body--xs: 12px;
--Font_weight--Italic: 400px;
--Font_weight--Medium: 500px;
--Font_weight--Bold: 700px;
--Font_family--Body: Inter;
--Paragraph_spacing--Heading--2xl: 60px;
--Paragraph_spacing--Heading--xl: 48px;
--Paragraph_spacing--Heading--lg: 40px;
--Paragraph_spacing--Heading--md: 32px;
--Paragraph_spacing--Heading--sm: 24px;
--Paragraph_spacing--Heading--xs: 20px;
--typography_components--h1--font-family: Inter;
--typography_components--h2--font-family: Inter;
--typography_components--h2--font-size: 30px;
--typography_components--h2--line-height: 36px;
--typography_components--h2--font-weight: 600;
--typography_components--h2--letter-spacing: -0.4000000059604645px;
--typography_components--h1--font-size: 36px;
--typography_components--h1--font-size-lg: 48px;
--typography_components--h1--line-height: 40px;
--typography_components--h1--font-weight: 800;
--typography_components--h1--letter-spacing: -0.4000000059604645px;
--typography_components--h3--font-family: Inter;
--typography_components--h3--font-size: 24px;
--typography_components--h3--line-height: 32px;
--typography_components--h3--font-weight: 600;
--typography_components--h3--letter-spacing: -0.4000000059604645px;
--typography_components--h4--font-family: Inter;
--typography_components--h4--font-size: 20px;
--typography_components--h4--line-height: 28px;
--typography_components--h4--font-weight: 600;
--typography_components--h4--letter-spacing: -0.4000000059604645px;
--typography_components--p--font-family: Inter;
--typography_components--p--font-size: 16px;
--typography_components--p--line-height: 28px;
--typography_components--p--font-weight: 400;
--typography_components--p--letter-spacing: 0px;
--typography_components--blockquote--font-family: Inter;
--typography_components--blockquote--font-size: 16px;
--typography_components--blockquote--line-height: 24px;
--typography_components--blockquote--letter-spacing: 0px;
--typography_components--blockquote--font-style: italic;
--typography_components--list--font-family: Inter;
--typography_components--list--font-size: 16px;
--typography_components--list--line-height: 28px;
--typography_components--list--letter-spacing: 0px;
--typography_components--inline_code--font-family: Menlo;
--typography_components--inline_code--font-size: 14px;
--typography_components--inline_code--line-height: 20px;
--typography_components--inline_code--font-weight: 600;
--typography_components--inline_code--letter-spacing: 0px;
--typography_components--lead--font-family: Inter;
--typography_components--lead--font-size: 20px;
--typography_components--lead--line-height: 28px;
--typography_components--lead--font-weight: 400;
--typography_components--lead--letter-spacing: 0px;
--typography_components--large--font-family: Inter;
--typography_components--large--font-size: 18px;
--typography_components--large--line-height: 28px;
--typography_components--large--font-weight: 600;
--typography_components--large--letter-spacing: 0px;
--typography_components--small--font-family: Inter;
--typography_components--small--font-size: 14px;
--typography_components--small--line-height: 14px;
--typography_components--small--font-weight: 500;
--typography_components--table--font-family: Inter;
--typography_components--table--font-size: 16px;
--typography_components--table--font-weight: 400;
--typography_components--table--font-weight-bold: 700;
--typography_components--table--letter-spacing: 0px;
/* Spacing and sizing: Desktop */
--Border_width--sm: 1px;
--Border_width--md: 2px;
--Border_width--lg: 3px;
--Radius--4xs: 4px;
--Radius--3xs: 8px;
--Radius--2xs: 12px;
--Radius--xs: 16px;
--Radius--sm: 24px;
--Radius--md: 32px;
--Radius--lg: 40px;
--Radius--xl: 48px;
--Radius--2xl: 56px;
--Radius--3xl: 64px;
--Radius--round: 999px;
--Spacing--5xs: 4px;
--Spacing--4xs: 8px;
--Spacing--3xs: 12px;
--Spacing--2xs: 16px;
--Spacing--xs: 24px;
--Spacing--sm: 32px;
--Spacing--md: 40px;
--Spacing--lg: 48px;
--Spacing--xl: 56px;
--Spacing--2xl: 64px;
--Spacing--3xl: 72px;
--Spacing--4xl: 80px;
--Spacing--5xl: 88px;
--Spacing--6xl: 96px;
--Spacing--7xl: 104px;
--Spacing--8xl: 112px;
--Sizing--5xs: 4px;
--Sizing--4xs: 8px;
--Sizing--3xs: 12px;
--Sizing--2xs: 16px;
--Sizing--xs: 24px;
--Sizing--sm: 32px;
--Sizing--md: 40px;
--Sizing--lg: 48px;
--Sizing--xl: 56px;
--Sizing--2xl: 64px;
--Sizing--3xl: 72px;
--Sizing--4xl: 80px;
--Sizing--5xl: 88px;
/* Color: Light mode */
--Opacity--Red--Red-100: var(--Primitive--Red--600);
--Opacity--Red--Red-80: hsla(0, 72%, 51%, 0.8);
--Opacity--Red--Red-60: hsla(0, 72%, 51%, 0.6);
--Opacity--Red--Red-40: hsla(0, 72%, 51%, 0.4);
--Opacity--Red--Red-20: hsla(0, 72%, 51%, 0.2);
--Opacity--Red--Red-10: hsla(0, 72%, 51%, 0.1);
--Opacity--Green--Green-100: var(--Primitive--Green--600);
--Opacity--Green--Green-80: hsla(142, 76%, 36%, 0.8);
--Opacity--Green--Green-60: hsla(142, 76%, 36%, 0.6);
--Opacity--Green--Green-40: hsla(142, 76%, 36%, 0.4);
--Opacity--Green--Green-20: hsla(142, 76%, 36%, 0.2);
--Opacity--Green--Green-10: hsla(142, 76%, 36%, 0.1);
--Opacity--Yellow--Yellow-100: var(--Primitive--Amber--400);
--Opacity--Yellow--Yellow-80: hsla(48, 96%, 53%, 0.8);
--Opacity--Yellow--Yellow-60: hsla(48, 96%, 53%, 0.6);
--Opacity--Yellow--Yellow-40: hsla(48, 96%, 53%, 0.4);
--Opacity--Yellow--Yellow-20: hsla(48, 96%, 53%, 0.2);
--Opacity--Yellow--Yellow-10: hsla(48, 96%, 53%, 0.1);
--Opacity--Violet--Violet-100: var(--Primitive--Violet--500);
--Opacity--Violet--Violet-80: hsla(258, 90%, 66%, 0.8);
--Opacity--Violet--Violet-60: hsla(258, 90%, 66%, 0.6);
--Opacity--Violet--Violet-40: hsla(258, 90%, 66%, 0.4);
--Opacity--Violet--Violet-20: hsla(258, 90%, 66%, 0.2);
--Opacity--Violet--Violet-10: hsla(258, 90%, 66%, 0.1);
--Opacity--Indigo--Indigo-100: var(--Primitive--Indigo--500);
--Opacity--Indigo--Indigo-80: hsla(239, 84%, 67%, 0.8);
--Opacity--Indigo--Indigo-60: hsla(239, 84%, 67%, 0.6);
--Opacity--Indigo--Indigo-40: hsla(239, 84%, 67%, 0.4);
--Opacity--Indigo--Indigo-20: hsla(239, 84%, 67%, 0.2);
--Opacity--Indigo--Indigo-10: hsla(239, 84%, 67%, 0.1);
--Opacity--Blue--Blue-100: var(--Primitive--Blue--500);
--Opacity--Blue--Blue-80: hsla(217, 91%, 60%, 0.8);
--Opacity--Blue--Blue-60: hsla(217, 91%, 60%, 0.6);
--Opacity--Blue--Blue-40: hsla(217, 91%, 60%, 0.4);
--Opacity--Blue--Blue-20: hsla(217, 91%, 60%, 0.2);
--Opacity--Blue--Blue-10: hsla(217, 91%, 60%, 0.1);
--Opacity--Grey--Grey-100: var(--Primitive--Gray--500);
--Opacity--Grey--Grey-80: hsla(220, 9%, 46%, 0.8);
--Opacity--Grey--Grey-60: hsla(220, 9%, 46%, 0.6);
--Opacity--Grey--Grey-40: hsla(220, 9%, 46%, 0.4);
--Opacity--Grey--Grey-20: hsla(220, 9%, 46%, 0.2);
--Opacity--Grey--Grey-10: hsla(220, 9%, 46%, 0.1);
--Opacity--White--White-100: var(--Primitive--White);
--Opacity--White--White-80: hsla(0, 0%, 100%, 0.8);
--Opacity--White--White-60: hsla(0, 0%, 100%, 0.6);
--Opacity--White--White-40: hsla(0, 0%, 100%, 0.4);
--Opacity--White--White-20: hsla(0, 0%, 100%, 0.2);
--Opacity--White--White-10: hsla(0, 0%, 100%, 0.1);
--Opacity--White--White-0: hsla(0, 0%, 100%, 0);
--Status--Error--colorErrorBg: var(--color--Red--50);
--Status--Error--colorErrorBgHover: var(--color--Red--100);
--Status--Error--colorErrorBorder: var(--color--Red--200);
--Status--Error--colorErrorBorderHover: var(--color--Red--300);
--Status--Error--colorErrorBase: var(--color--Red--500);
--Status--Error--colorErrorActive: var(--color--Red--600);
--Status--Error--colorErrorTextHover: var(--color--Red--700);
--Status--Error--colorErrorText: var(--color--Red--800);
--Status--Success--colorSuccessBg: var(--color--Green--50);
--Status--Success--colorSuccessBgHover: var(--color--Green--100);
--Status--Success--colorSuccessBase: var(--color--Green--500);
--Status--Success--colorSuccessTextHover: var(--color--Green--700);
--Status--Warning--colorWarningBg: var(--color--Yellow--50);
--Status--Warning--colorWarningBgHover: var(--color--Yellow--100);
--Status--Warning--colorWarningBase: var(--color--Yellow--500);
--Status--Warning--colorWarningActive: var(--color--Yellow--600);
--Status--Warning--colorWarningTextHover: var(--color--Yellow--700);
--Primitive--Black: hsla(0, 0%, 0%, 1);
--Primitive--White: hsla(0, 0%, 100%, 1);
--Brand--Base_Colors--Primary: var(--Primitive--Lime--500);
--Primitive--Neutral--50: hsla(0, 0%, 98%, 1);
--Primitive--Neutral--100: hsla(0, 0%, 96%, 1);
--Primitive--Neutral--200: hsla(0, 0%, 90%, 1);
--Primitive--Neutral--300: hsla(0, 0%, 83%, 1);
--Primitive--Neutral--400: hsla(0, 0%, 64%, 1);
--Primitive--Neutral--500: hsla(0, 0%, 45%, 1);
--Primitive--Neutral--600: hsla(215, 14%, 34%, 1);
--Primitive--Neutral--700: hsla(0, 0%, 25%, 1);
--Primitive--Neutral--800: hsla(0, 0%, 15%, 1);
--Primitive--Neutral--900: hsla(0, 0%, 9%, 1);
--Primitive--Neutral--950: hsla(0, 0%, 4%, 1);
--Primitive--Stone--50: hsla(60, 9%, 98%, 1);
--Primitive--Stone--100: hsla(60, 5%, 96%, 1);
--Primitive--Stone--200: hsla(20, 6%, 90%, 1);
--Primitive--Stone--300: hsla(24, 6%, 83%, 1);
--Primitive--Stone--400: hsla(24, 5%, 64%, 1);
--Primitive--Stone--500: hsla(25, 5%, 45%, 1);
--Primitive--Stone--600: hsla(33, 5%, 32%, 1);
--Primitive--Stone--700: hsla(30, 6%, 25%, 1);
--Primitive--Stone--800: hsla(12, 6%, 15%, 1);
--Primitive--Stone--900: hsla(24, 10%, 10%, 1);
--Primitive--Stone--950: hsla(20, 14%, 4%, 1);
--Primitive--Zinc--50: hsla(0, 0%, 98%, 1);
--Primitive--Zinc--100: hsla(240, 5%, 96%, 1);
--Primitive--Zinc--200: hsla(240, 6%, 90%, 1);
--Primitive--Zinc--300: hsla(240, 5%, 84%, 1);
--Primitive--Zinc--400: hsla(240, 5%, 65%, 1);
--Primitive--Zinc--500: hsla(240, 4%, 46%, 1);
--Primitive--Zinc--600: hsla(240, 5%, 34%, 1);
--Primitive--Zinc--700: hsla(240, 5%, 26%, 1);
--Primitive--Zinc--800: hsla(240, 4%, 16%, 1);
--Primitive--Zinc--900: hsla(240, 6%, 10%, 1);
--Primitive--Zinc--950: hsla(240, 10%, 4%, 1);
--Primitive--Slate--50: hsla(210, 40%, 98%, 1);
--Primitive--Slate--100: hsla(210, 40%, 96%, 1);
--Primitive--Slate--200: hsla(214, 32%, 91%, 1);
--Primitive--Slate--300: hsla(213, 27%, 84%, 1);
--Primitive--Slate--400: hsla(215, 20%, 65%, 1);
--Primitive--Slate--500: hsla(215, 16%, 47%, 1);
--Primitive--Slate--600: hsla(215, 19%, 35%, 1);
--Primitive--Slate--700: hsla(215, 25%, 27%, 1);
--Primitive--Slate--800: hsla(217, 33%, 17%, 1);
--Primitive--Slate--900: hsla(222, 47%, 11%, 1);
--Primitive--Slate--950: hsla(229, 84%, 5%, 1);
--Primitive--Gray--50: hsla(210, 20%, 98%, 1);
--Primitive--Gray--100: hsla(220, 14%, 96%, 1);
--Primitive--Gray--200: hsla(220, 13%, 91%, 1);
--Primitive--Gray--300: hsla(216, 12%, 84%, 1);
--Primitive--Gray--400: hsla(218, 11%, 65%, 1);
--Primitive--Gray--500: hsla(220, 9%, 46%, 1);
--Primitive--Gray--600: hsla(0, 0%, 32%, 1);
--Primitive--Gray--700: hsla(217, 19%, 27%, 1);
--Primitive--Gray--800: hsla(215, 28%, 17%, 1);
--Primitive--Gray--900: hsla(221, 39%, 11%, 1);
--Primitive--Gray--950: hsla(224, 71%, 4%, 1);
--Primitive--Red--50: hsla(0, 86%, 97%, 1);
--Primitive--Red--100: hsla(0, 93%, 94%, 1);
--Primitive--Red--200: hsla(0, 96%, 89%, 1);
--Primitive--Red--300: hsla(0, 94%, 82%, 1);
--Primitive--Red--400: hsla(0, 91%, 71%, 1);
--Primitive--Red--500: hsla(0, 84%, 60%, 1);
--Primitive--Red--600: hsla(0, 72%, 51%, 1);
--Primitive--Red--700: hsla(0, 74%, 42%, 1);
--Primitive--Red--800: hsla(0, 70%, 35%, 1);
--Primitive--Red--900: hsla(0, 63%, 31%, 1);
--Primitive--Red--950: hsla(0, 75%, 15%, 1);
--Primitive--Orange--50: hsla(33, 100%, 96%, 1);
--Primitive--Orange--100: hsla(34, 100%, 92%, 1);
--Primitive--Orange--200: hsla(32, 98%, 83%, 1);
--Primitive--Orange--300: hsla(31, 97%, 72%, 1);
--Primitive--Orange--400: hsla(27, 96%, 61%, 1);
--Primitive--Orange--500: hsla(25, 95%, 53%, 1);
--Primitive--Orange--600: hsla(21, 90%, 48%, 1);
--Primitive--Orange--700: hsla(17, 88%, 40%, 1);
--Primitive--Orange--800: hsla(15, 79%, 34%, 1);
--Primitive--Orange--900: hsla(15, 75%, 28%, 1);
--Primitive--Orange--950: hsla(13, 81%, 15%, 1);
--Primitive--Amber--50: hsla(48, 100%, 96%, 1);
--Primitive--Amber--100: hsla(48, 96%, 89%, 1);
--Primitive--Amber--200: hsla(48, 97%, 77%, 1);
--Primitive--Amber--300: hsla(46, 97%, 65%, 1);
--Primitive--Amber--400: hsla(43, 96%, 56%, 1);
--Primitive--Amber--500: hsla(38, 92%, 50%, 1);
--Primitive--Amber--600: hsla(32, 95%, 44%, 1);
--Primitive--Amber--700: hsla(26, 90%, 37%, 1);
--Primitive--Amber--800: hsla(23, 83%, 31%, 1);
--Primitive--Amber--900: hsla(22, 78%, 26%, 1);
--Primitive--Amber--950: hsla(21, 92%, 14%, 1);
--Primitive--Yellow--50: hsla(55, 92%, 95%, 1);
--Primitive--Yellow--100: hsla(55, 97%, 88%, 1);
--Primitive--Yellow--200: hsla(53, 98%, 77%, 1);
--Primitive--Yellow--300: hsla(50, 98%, 64%, 1);
--Primitive--Yellow--400: hsla(48, 96%, 53%, 1);
--Primitive--Yellow--500: hsla(45, 93%, 47%, 1);
--Primitive--Yellow--600: hsla(41, 96%, 40%, 1);
--Primitive--Yellow--700: hsla(35, 92%, 33%, 1);
--Primitive--Yellow--800: hsla(32, 81%, 29%, 1);
--Primitive--Yellow--900: hsla(28, 73%, 26%, 1);
--Primitive--Yellow--950: hsla(26, 83%, 14%, 1);
--Primitive--Lime--50: hsla(78, 92%, 95%, 1);
--Primitive--Lime--100: hsla(80, 89%, 89%, 1);
--Primitive--Lime--200: hsla(81, 88%, 80%, 1);
--Primitive--Lime--300: hsla(82, 85%, 67%, 1);
--Primitive--Lime--400: hsla(83, 78%, 55%, 1);
--Primitive--Lime--500: hsla(84, 81%, 44%, 1);
--Primitive--Lime--600: hsla(85, 85%, 35%, 1);
--Primitive--Lime--700: hsla(86, 78%, 27%, 1);
--Primitive--Lime--800: hsla(86, 69%, 23%, 1);
--Primitive--Lime--900: hsla(88, 61%, 20%, 1);
--Primitive--Lime--950: hsla(89, 80%, 10%, 1);
--Primitive--Green--50: hsla(138, 76%, 97%, 1);
--Primitive--Green--100: hsla(141, 84%, 93%, 1);
--Primitive--Green--200: hsla(141, 79%, 85%, 1);
--Primitive--Green--300: hsla(142, 77%, 73%, 1);
--Primitive--Green--400: hsla(142, 69%, 58%, 1);
--Primitive--Green--500: hsla(142, 71%, 45%, 1);
--Primitive--Green--600: hsla(142, 76%, 36%, 1);
--Primitive--Green--700: hsla(142, 72%, 29%, 1);
--Primitive--Green--800: hsla(143, 64%, 24%, 1);
--Primitive--Green--900: hsla(144, 61%, 20%, 1);
--Primitive--Green--950: hsla(145, 80%, 10%, 1);
--Primitive--Emerald--50: hsla(152, 81%, 96%, 1);
--Primitive--Emerald--100: hsla(149, 80%, 90%, 1);
--Primitive--Emerald--200: hsla(152, 76%, 80%, 1);
--Primitive--Emerald--300: hsla(156, 72%, 67%, 1);
--Primitive--Emerald--400: hsla(158, 64%, 52%, 1);
--Primitive--Emerald--500: hsla(160, 84%, 39%, 1);
--Primitive--Emerald--600: hsla(161, 94%, 30%, 1);
--Primitive--Emerald--700: hsla(163, 94%, 24%, 1);
--Primitive--Emerald--800: hsla(163, 88%, 20%, 1);
--Primitive--Emerald--900: hsla(164, 86%, 16%, 1);
--Primitive--Emerald--950: hsla(166, 91%, 9%, 1);
--Primitive--Teal--50: hsla(166, 76%, 97%, 1);
--Primitive--Teal--100: hsla(167, 85%, 89%, 1);
--Primitive--Teal--200: hsla(168, 84%, 78%, 1);
--Primitive--Teal--300: hsla(171, 77%, 64%, 1);
--Primitive--Teal--400: hsla(172, 66%, 50%, 1);
--Primitive--Teal--500: hsla(173, 80%, 40%, 1);
--Primitive--Teal--600: hsla(175, 84%, 32%, 1);
--Primitive--Teal--700: hsla(175, 77%, 26%, 1);
--Primitive--Teal--800: hsla(176, 69%, 22%, 1);
--Primitive--Teal--900: hsla(176, 61%, 19%, 1);
--Primitive--Teal--950: hsla(179, 84%, 10%, 1);
--Primitive--Cyan--50: hsla(183, 100%, 96%, 1);
--Primitive--Cyan--100: hsla(185, 96%, 90%, 1);
--Primitive--Cyan--200: hsla(186, 94%, 82%, 1);
--Primitive--Cyan--300: hsla(187, 92%, 69%, 1);
--Primitive--Cyan--400: hsla(188, 86%, 53%, 1);
--Primitive--Cyan--500: hsla(189, 94%, 43%, 1);
--Primitive--Cyan--600: hsla(192, 91%, 36%, 1);
--Primitive--Cyan--700: hsla(193, 82%, 31%, 1);
--Primitive--Cyan--800: hsla(194, 70%, 27%, 1);
--Primitive--Cyan--900: hsla(196, 64%, 24%, 1);
--Primitive--Cyan--950: hsla(197, 79%, 15%, 1);
--Primitive--Sky--50: hsla(204, 100%, 97%, 1);
--Primitive--Sky--100: hsla(204, 94%, 94%, 1);
--Primitive--Sky--200: hsla(201, 94%, 86%, 1);
--Primitive--Sky--300: hsla(199, 95%, 74%, 1);
--Primitive--Sky--400: hsla(198, 93%, 60%, 1);
--Primitive--Sky--500: hsla(199, 89%, 48%, 1);
--Primitive--Sky--600: hsla(200, 98%, 39%, 1);
--Primitive--Sky--700: hsla(201, 96%, 32%, 1);
--Primitive--Sky--800: hsla(201, 90%, 27%, 1);
--Primitive--Sky--900: hsla(202, 80%, 24%, 1);
--Primitive--Sky--950: hsla(204, 80%, 16%, 1);
--Primitive--Blue--50: hsla(214, 100%, 97%, 1);
--Primitive--Blue--100: hsla(214, 95%, 93%, 1);
--Primitive--Blue--200: hsla(213, 97%, 87%, 1);
--Primitive--Blue--300: hsla(212, 96%, 78%, 1);
--Primitive--Blue--400: hsla(213, 94%, 68%, 1);
--Primitive--Blue--500: hsla(217, 91%, 60%, 1);
--Primitive--Blue--600: hsla(221, 83%, 53%, 1);
--Primitive--Blue--700: hsla(224, 76%, 48%, 1);
--Primitive--Blue--800: hsla(226, 71%, 40%, 1);
--Primitive--Blue--900: hsla(224, 64%, 33%, 1);
--Primitive--Blue--950: hsla(226, 57%, 21%, 1);
--Primitive--Indigo--50: hsla(226, 100%, 97%, 1);
--Primitive--Indigo--100: hsla(226, 100%, 94%, 1);
--Primitive--Indigo--200: hsla(228, 96%, 89%, 1);
--Primitive--Indigo--300: hsla(230, 94%, 82%, 1);
--Primitive--Indigo--400: hsla(234, 89%, 74%, 1);
--Primitive--Indigo--500: hsla(239, 84%, 67%, 1);
--Primitive--Indigo--600: hsla(243, 75%, 59%, 1);
--Primitive--Indigo--700: hsla(245, 58%, 51%, 1);
--Primitive--Indigo--800: hsla(244, 55%, 41%, 1);
--Primitive--Indigo--900: hsla(242, 47%, 34%, 1);
--Primitive--Indigo--950: hsla(244, 47%, 20%, 1);
--Primitive--Violet--50: hsla(250, 100%, 98%, 1);
--Primitive--Violet--100: hsla(251, 91%, 95%, 1);
--Primitive--Violet--200: hsla(251, 95%, 92%, 1);
--Primitive--Violet--300: hsla(253, 95%, 85%, 1);
--Primitive--Violet--400: hsla(255, 92%, 76%, 1);
--Primitive--Violet--500: hsla(258, 90%, 66%, 1);
--Primitive--Violet--600: hsla(262, 83%, 58%, 1);
--Primitive--Violet--700: hsla(263, 70%, 50%, 1);
--Primitive--Violet--800: hsla(263, 69%, 42%, 1);
--Primitive--Violet--900: hsla(264, 67%, 35%, 1);
--Primitive--Violet--950: hsla(262, 78%, 23%, 1);
--Primitive--Purple--50: hsla(270, 100%, 98%, 1);
--Primitive--Purple--100: hsla(269, 100%, 95%, 1);
--Primitive--Purple--200: hsla(269, 100%, 92%, 1);
--Primitive--Purple--300: hsla(269, 97%, 85%, 1);
--Primitive--Purple--400: hsla(270, 95%, 75%, 1);
--Primitive--Purple--500: hsla(271, 91%, 65%, 1);
--Primitive--Purple--600: hsla(271, 81%, 56%, 1);
--Primitive--Purple--700: hsla(272, 72%, 47%, 1);
--Primitive--Purple--800: hsla(273, 67%, 39%, 1);
--Primitive--Purple--900: hsla(274, 66%, 32%, 1);
--Primitive--Purple--950: hsla(274, 87%, 21%, 1);
--Primitive--Fuchsia--50: hsla(289, 100%, 98%, 1);
--Primitive--Fuchsia--100: hsla(287, 100%, 95%, 1);
--Primitive--Fuchsia--200: hsla(288, 96%, 91%, 1);
--Primitive--Fuchsia--300: hsla(291, 93%, 83%, 1);
--Primitive--Fuchsia--400: hsla(292, 91%, 73%, 1);
--Primitive--Fuchsia--500: hsla(292, 84%, 61%, 1);
--Primitive--Fuchsia--600: hsla(293, 69%, 49%, 1);
--Primitive--Fuchsia--700: hsla(295, 72%, 40%, 1);
--Primitive--Fuchsia--800: hsla(295, 70%, 33%, 1);
--Primitive--Fuchsia--900: hsla(297, 64%, 28%, 1);
--Primitive--Fuchsia--950: hsla(297, 90%, 16%, 1);
--Primitive--Pink--50: hsla(327, 73%, 97%, 1);
--Primitive--Pink--100: hsla(326, 78%, 95%, 1);
--Primitive--Pink--200: hsla(326, 85%, 90%, 1);
--Primitive--Pink--300: hsla(327, 87%, 82%, 1);
--Primitive--Pink--400: hsla(329, 86%, 70%, 1);
--Primitive--Pink--500: hsla(330, 81%, 60%, 1);
--Primitive--Pink--600: hsla(333, 71%, 51%, 1);
--Primitive--Pink--700: hsla(335, 78%, 42%, 1);
--Primitive--Pink--800: hsla(336, 74%, 35%, 1);
--Primitive--Pink--900: hsla(336, 69%, 30%, 1);
--Primitive--Pink--950: hsla(336, 84%, 17%, 1);
--Primitive--Rose--50: hsla(356, 100%, 97%, 1);
--Primitive--Rose--100: hsla(356, 100%, 95%, 1);
--Primitive--Rose--200: hsla(353, 96%, 90%, 1);
--Primitive--Rose--300: hsla(353, 96%, 82%, 1);
--Primitive--Rose--400: hsla(351, 95%, 71%, 1);
--Primitive--Rose--500: hsla(350, 89%, 60%, 1);
--Primitive--Rose--600: hsla(347, 77%, 50%, 1);
--Primitive--Rose--700: hsla(345, 83%, 41%, 1);
--Primitive--Rose--800: hsla(343, 80%, 35%, 1);
--Primitive--Rose--900: hsla(342, 75%, 30%, 1);
--Primitive--Rose--950: hsla(343, 88%, 16%, 1);
--Brand--Base_Colors--Destructive: var(--Primitive--Red--500);
--Brand--Base_Colors--Success: var(--Primitive--Green--500);
--Brand--Base_Colors--Warning: var(--Primitive--Amber--500);
--Brand--Base_Colors--White: var(--Primitive--White);
--Brand--Base_Colors--Black: var(--Primitive--Black);
--Brand--Semantic_Colors--Background: var(--Primitive--Zinc--50); /*页面背景色:应用在整个页面的最底层。*/
--Brand--Semantic_Colors--Background-subtle: hsla(
0,
0%,
0%,
0.02
); /*细微背景色:用于需要与主背景有微弱区分的区域,如代码块背景。*/
--Brand--Semantic_Colors--Foreground: hsla(0, 0%, 0%, 0.9); /*主要前景/文字色:用于正文、标题等。*/
--Brand--Semantic_Colors--Foreground-secondary: hsla(0, 0%, 0%, 0.6); /*次要前景/文字色:用于辅助性文本、描述。*/
--Brand--Semantic_Colors--Foreground-muted: hsla(0, 0%, 0%, 0.4); /*静默前景/文字色:用于禁用状态的文字、占位符。*/
--Brand--Semantic_Colors--Border: hsla(0, 0%, 0%, 0.1); /*默认边框色:用于卡片、输入框、分隔线。*/
--Brand--Semantic_Colors--Border-hover: hsla(0, 0%, 0%, 0.2); /*激活边框色:用于元素被按下或激活时的边框。*/
--Brand--Semantic_Colors--Border-active: hsla(0, 0%, 0%, 0.3); /*激活边框色:用于元素被按下或激活时的边框。*/
--Brand--Semantic_Colors--Ring: hsla(
84,
81%,
44%,
0.4
); /*聚焦环颜色:用于输入框等元素在聚焦 (Focus) 状态下的外发光。*/
--Brand--UI_Element_Colors--Modal--Backdrop: hsla(0, 0%, 0%, 0.4);
--Brand--UI_Element_Colors--Modal--Thumb: hsla(0, 0%, 0%, 0.2);
--Brand--UI_Element_Colors--Modal--Thumb_Hover: hsla(0, 0%, 0%, 0.3);
--Brand--UI_Element_Colors--Icon--Default: var(--Brand--Semantic_Colors--Foreground-secondary);
--Brand--UI_Element_Colors--Icon--Hover: var(--Brand--Semantic_Colors--Foreground);
--Brand--UI_Element_Colors--Input_Select--Background: var(--Brand--Base_Colors--White);
--Brand--UI_Element_Colors--Input_Select--Border: var(--Brand--Semantic_Colors--Border);
--Brand--UI_Element_Colors--Input_Select--Border_Hover: var(--Brand--Semantic_Colors--Border-hover);
--Brand--UI_Element_Colors--Input_Select--Border_Focus: var(--Brand--Base_Colors--Primary);
--Brand--UI_Element_Colors--Primary_Button--Background: var(--Brand--Base_Colors--Primary);
--Brand--UI_Element_Colors--Card_Container--Background: var(--Brand--Base_Colors--White);
--Brand--UI_Element_Colors--Card_Container--Border: var(--Brand--Semantic_Colors--Border);
--Brand--UI_Element_Colors--Ghost_Button--Background: hsla(0, 0%, 0%, 0);
--Brand--UI_Element_Colors--Ghost_Button--Text: var(--Brand--Semantic_Colors--Foreground);
--Brand--UI_Element_Colors--Ghost_Button--Background_Hover: hsla(0, 0%, 0%, 0.05);
--Brand--UI_Element_Colors--Ghost_Button--Background_Active: hsla(0, 0%, 0%, 0.1);
--Brand--UI_Element_Colors--Secondary_Button--Background: hsla(0, 0%, 0%, 0.05);
--Brand--UI_Element_Colors--Secondary_Button--Text: var(--Brand--Semantic_Colors--Foreground);
--Brand--UI_Element_Colors--Secondary_Button--Background_Hover: hsla(0, 0%, 0%, 0.85);
--Brand--UI_Element_Colors--Secondary_Button--Background_Active: hsla(0, 0%, 0%, 0.7);
--Brand--UI_Element_Colors--Secondary_Button--Border: var(--Brand--Semantic_Colors--Border);
--Brand--UI_Element_Colors--Primary_Button--Text: var(--Brand--Base_Colors--White);
--Brand--UI_Element_Colors--Primary_Button--Background_Hover: hsla(84, 81%, 44%, 0.85);
--Brand--UI_Element_Colors--Primary_Button--2nd_Background: hsla(84, 81%, 44%, 0.1);
--Brand--UI_Element_Colors--Primary_Button--3rd_Background: hsla(84, 81%, 44%, 0.05);
--Brand--UI_Element_Colors--Primary_Button--Background_Active: hsla(84, 81%, 44%, 0.7);
--Boolean: false;
/* Color: Dark mode */
--Opacity--Red--Red-100: var(--Primitive--Red--600);
--Opacity--Red--Red-80: hsla(0, 72%, 51%, 0.8);
--Opacity--Red--Red-60: hsla(0, 72%, 51%, 0.6);
--Opacity--Red--Red-40: hsla(0, 72%, 51%, 0.4);
--Opacity--Red--Red-20: hsla(0, 72%, 51%, 0.2);
--Opacity--Red--Red-10: hsla(0, 72%, 51%, 0.1);
--Opacity--Green--Green-100: var(--Primitive--Green--600);
--Opacity--Green--Green-80: hsla(142, 76%, 36%, 0.8);
--Opacity--Green--Green-60: hsla(142, 76%, 36%, 0.6);
--Opacity--Green--Green-40: hsla(142, 76%, 36%, 0.4);
--Opacity--Green--Green-20: hsla(142, 76%, 36%, 0.2);
--Opacity--Green--Green-10: hsla(142, 76%, 36%, 0.1);
--Opacity--Yellow--Yellow-100: var(--Primitive--Yellow--400);
--Opacity--Yellow--Yellow-80: hsla(48, 96%, 53%, 0.8);
--Opacity--Yellow--Yellow-60: hsla(48, 96%, 53%, 0.6);
--Opacity--Yellow--Yellow-40: hsla(48, 96%, 53%, 0.4);
--Opacity--Yellow--Yellow-20: hsla(48, 96%, 53%, 0.2);
--Opacity--Yellow--Yellow-10: hsla(48, 96%, 53%, 0.1);
--Opacity--Violet--Violet-100: var(--Primitive--Violet--500);
--Opacity--Violet--Violet-80: hsla(258, 90%, 66%, 0.8);
--Opacity--Violet--Violet-60: hsla(258, 90%, 66%, 0.6);
--Opacity--Violet--Violet-40: hsla(258, 90%, 66%, 0.4);
--Opacity--Violet--Violet-20: hsla(258, 90%, 66%, 0.2);
--Opacity--Violet--Violet-10: hsla(258, 90%, 66%, 0.1);
--Opacity--Indigo--Indigo-100: var(--Primitive--Indigo--500);
--Opacity--Indigo--Indigo-80: hsla(239, 84%, 67%, 0.8);
--Opacity--Indigo--Indigo-60: hsla(239, 84%, 67%, 0.6);
--Opacity--Indigo--Indigo-40: hsla(239, 84%, 67%, 0.4);
--Opacity--Indigo--Indigo-20: hsla(239, 84%, 67%, 0.2);
--Opacity--Indigo--Indigo-10: hsla(239, 84%, 67%, 0.1);
--Opacity--Blue--Blue-100: var(--Primitive--Blue--500);
--Opacity--Blue--Blue-80: hsla(217, 91%, 60%, 0.8);
--Opacity--Blue--Blue-60: hsla(217, 91%, 60%, 0.6);
--Opacity--Blue--Blue-40: hsla(217, 91%, 60%, 0.4);
--Opacity--Blue--Blue-20: hsla(217, 91%, 60%, 0.2);
--Opacity--Blue--Blue-10: hsla(217, 91%, 60%, 0.1);
--Opacity--Grey--Grey-100: var(--Primitive--Gray--500);
--Opacity--Grey--Grey-80: hsla(220, 9%, 46%, 0.8);
--Opacity--Grey--Grey-60: hsla(220, 9%, 46%, 0.6);
--Opacity--Grey--Grey-40: hsla(220, 9%, 46%, 0.4);
--Opacity--Grey--Grey-20: hsla(220, 9%, 46%, 0.2);
--Opacity--Grey--Grey-10: hsla(220, 9%, 46%, 0.1);
--Opacity--White--White-100: var(--Primitive--White);
--Opacity--White--White-80: hsla(0, 0%, 100%, 0.8);
--Opacity--White--White-60: hsla(0, 0%, 100%, 0.6);
--Opacity--White--White-40: hsla(0, 0%, 100%, 0.4);
--Opacity--White--White-20: hsla(0, 0%, 100%, 0.2);
--Opacity--White--White-10: hsla(0, 0%, 100%, 0.1);
--Opacity--White--White-0: hsla(0, 0%, 100%, 0);
--Status--Error--colorErrorBg: var(--color--Red--900);
--Status--Error--colorErrorBgHover: var(--color--Red--800);
--Status--Error--colorErrorBorder: var(--color--Red--700);
--Status--Error--colorErrorBorderHover: var(--color--Red--600);
--Status--Error--colorErrorBase: var(--color--Red--400);
--Status--Error--colorErrorActive: var(--color--Red--300);
--Status--Error--colorErrorTextHover: var(--color--Red--200);
--Status--Error--colorErrorText: var(--color--Red--100);
--Status--Success--colorSuccessBg: var(--color--Green--900);
--Status--Success--colorSuccessBgHover: var(--color--Green--800);
--Status--Success--colorSuccessBase: var(--color--Green--400);
--Status--Success--colorSuccessTextHover: var(--color--Green--200);
--Status--Warning--colorWarningBg: var(--color--Yellow--900);
--Status--Warning--colorWarningBgHover: var(--color--Yellow--800);
--Status--Warning--colorWarningBase: var(--color--Yellow--400);
--Status--Warning--colorWarningActive: var(--color--Yellow--300);
--Status--Warning--colorWarningTextHover: var(--color--Yellow--200);
--Primitive--Black: hsla(0, 0%, 0%, 1);
--Primitive--White: hsla(0, 0%, 100%, 1);
--Brand--Base_Colors--Primary: var(--Primitive--Lime--500);
--Primitive--Neutral--50: hsla(0, 0%, 98%, 1);
--Primitive--Neutral--100: hsla(0, 0%, 96%, 1);
--Primitive--Neutral--200: hsla(0, 0%, 90%, 1);
--Primitive--Neutral--300: hsla(0, 0%, 83%, 1);
--Primitive--Neutral--400: hsla(0, 0%, 64%, 1);
--Primitive--Neutral--500: hsla(0, 0%, 45%, 1);
--Primitive--Neutral--600: hsla(215, 14%, 34%, 1);
--Primitive--Neutral--700: hsla(0, 0%, 25%, 1);
--Primitive--Neutral--800: hsla(0, 0%, 15%, 1);
--Primitive--Neutral--900: hsla(0, 0%, 9%, 1);
--Primitive--Neutral--950: hsla(0, 0%, 4%, 1);
--Primitive--Stone--50: hsla(60, 9%, 98%, 1);
--Primitive--Stone--100: hsla(60, 5%, 96%, 1);
--Primitive--Stone--200: hsla(20, 6%, 90%, 1);
--Primitive--Stone--300: hsla(24, 6%, 83%, 1);
--Primitive--Stone--400: hsla(24, 5%, 64%, 1);
--Primitive--Stone--500: hsla(25, 5%, 45%, 1);
--Primitive--Stone--600: hsla(33, 5%, 32%, 1);
--Primitive--Stone--700: hsla(30, 6%, 25%, 1);
--Primitive--Stone--800: hsla(12, 6%, 15%, 1);
--Primitive--Stone--900: hsla(24, 10%, 10%, 1);
--Primitive--Stone--950: hsla(20, 14%, 4%, 1);
--Primitive--Zinc--50: hsla(0, 0%, 98%, 1);
--Primitive--Zinc--100: hsla(240, 5%, 96%, 1);
--Primitive--Zinc--200: hsla(240, 6%, 90%, 1);
--Primitive--Zinc--300: hsla(240, 5%, 84%, 1);
--Primitive--Zinc--400: hsla(240, 5%, 65%, 1);
--Primitive--Zinc--500: hsla(240, 4%, 46%, 1);
--Primitive--Zinc--600: hsla(240, 5%, 34%, 1);
--Primitive--Zinc--700: hsla(240, 5%, 26%, 1);
--Primitive--Zinc--800: hsla(240, 4%, 16%, 1);
--Primitive--Zinc--900: hsla(240, 6%, 10%, 1);
--Primitive--Zinc--950: hsla(240, 10%, 4%, 1);
--Primitive--Slate--50: hsla(210, 40%, 98%, 1);
--Primitive--Slate--100: hsla(210, 40%, 96%, 1);
--Primitive--Slate--200: hsla(214, 32%, 91%, 1);
--Primitive--Slate--300: hsla(213, 27%, 84%, 1);
--Primitive--Slate--400: hsla(215, 20%, 65%, 1);
--Primitive--Slate--500: hsla(215, 16%, 47%, 1);
--Primitive--Slate--600: hsla(215, 19%, 35%, 1);
--Primitive--Slate--700: hsla(215, 25%, 27%, 1);
--Primitive--Slate--800: hsla(217, 33%, 17%, 1);
--Primitive--Slate--900: hsla(222, 47%, 11%, 1);
--Primitive--Slate--950: hsla(229, 84%, 5%, 1);
--Primitive--Gray--50: hsla(210, 20%, 98%, 1);
--Primitive--Gray--100: hsla(220, 14%, 96%, 1);
--Primitive--Gray--200: hsla(220, 13%, 91%, 1);
--Primitive--Gray--300: hsla(216, 12%, 84%, 1);
--Primitive--Gray--400: hsla(218, 11%, 65%, 1);
--Primitive--Gray--500: hsla(220, 9%, 46%, 1);
--Primitive--Gray--600: hsla(0, 0%, 32%, 1);
--Primitive--Gray--700: hsla(217, 19%, 27%, 1);
--Primitive--Gray--800: hsla(215, 28%, 17%, 1);
--Primitive--Gray--900: hsla(221, 39%, 11%, 1);
--Primitive--Gray--950: hsla(224, 71%, 4%, 1);
--Primitive--Red--50: hsla(0, 86%, 97%, 1);
--Primitive--Red--100: hsla(0, 93%, 94%, 1);
--Primitive--Red--200: hsla(0, 96%, 89%, 1);
--Primitive--Red--300: hsla(0, 94%, 82%, 1);
--Primitive--Red--400: hsla(0, 91%, 71%, 1);
--Primitive--Red--500: hsla(0, 84%, 60%, 1);
--Primitive--Red--600: hsla(0, 72%, 51%, 1);
--Primitive--Red--700: hsla(0, 74%, 42%, 1);
--Primitive--Red--800: hsla(0, 70%, 35%, 1);
--Primitive--Red--900: hsla(0, 63%, 31%, 1);
--Primitive--Red--950: hsla(0, 75%, 15%, 1);
--Primitive--Orange--50: hsla(33, 100%, 96%, 1);
--Primitive--Orange--100: hsla(34, 100%, 92%, 1);
--Primitive--Orange--200: hsla(32, 98%, 83%, 1);
--Primitive--Orange--300: hsla(31, 97%, 72%, 1);
--Primitive--Orange--400: hsla(27, 96%, 61%, 1);
--Primitive--Orange--500: hsla(25, 95%, 53%, 1);
--Primitive--Orange--600: hsla(21, 90%, 48%, 1);
--Primitive--Orange--700: hsla(17, 88%, 40%, 1);
--Primitive--Orange--800: hsla(15, 79%, 34%, 1);
--Primitive--Orange--900: hsla(15, 75%, 28%, 1);
--Primitive--Orange--950: hsla(13, 81%, 15%, 1);
--Primitive--Amber--50: hsla(48, 100%, 96%, 1);
--Primitive--Amber--100: hsla(48, 96%, 89%, 1);
--Primitive--Amber--200: hsla(48, 97%, 77%, 1);
--Primitive--Amber--300: hsla(46, 97%, 65%, 1);
--Primitive--Amber--400: hsla(43, 96%, 56%, 1);
--Primitive--Amber--500: hsla(38, 92%, 50%, 1);
--Primitive--Amber--600: hsla(32, 95%, 44%, 1);
--Primitive--Amber--700: hsla(26, 90%, 37%, 1);
--Primitive--Amber--800: hsla(23, 83%, 31%, 1);
--Primitive--Amber--900: hsla(22, 78%, 26%, 1);
--Primitive--Amber--950: hsla(21, 92%, 14%, 1);
--Primitive--Yellow--50: hsla(55, 92%, 95%, 1);
--Primitive--Yellow--100: hsla(55, 97%, 88%, 1);
--Primitive--Yellow--200: hsla(53, 98%, 77%, 1);
--Primitive--Yellow--300: hsla(50, 98%, 64%, 1);
--Primitive--Yellow--400: hsla(48, 96%, 53%, 1);
--Primitive--Yellow--500: hsla(45, 93%, 47%, 1);
--Primitive--Yellow--600: hsla(41, 96%, 40%, 1);
--Primitive--Yellow--700: hsla(35, 92%, 33%, 1);
--Primitive--Yellow--800: hsla(32, 81%, 29%, 1);
--Primitive--Yellow--900: hsla(28, 73%, 26%, 1);
--Primitive--Yellow--950: hsla(26, 83%, 14%, 1);
--Primitive--Lime--50: hsla(78, 92%, 95%, 1);
--Primitive--Lime--100: hsla(80, 89%, 89%, 1);
--Primitive--Lime--200: hsla(81, 88%, 80%, 1);
--Primitive--Lime--300: hsla(82, 85%, 67%, 1);
--Primitive--Lime--400: hsla(83, 78%, 55%, 1);
--Primitive--Lime--500: hsla(84, 81%, 44%, 1);
--Primitive--Lime--600: hsla(85, 85%, 35%, 1);
--Primitive--Lime--700: hsla(86, 78%, 27%, 1);
--Primitive--Lime--800: hsla(86, 69%, 23%, 1);
--Primitive--Lime--900: hsla(88, 61%, 20%, 1);
--Primitive--Lime--950: hsla(89, 80%, 10%, 1);
--Primitive--Green--50: hsla(138, 76%, 97%, 1);
--Primitive--Green--100: hsla(141, 84%, 93%, 1);
--Primitive--Green--200: hsla(141, 79%, 85%, 1);
--Primitive--Green--300: hsla(142, 77%, 73%, 1);
--Primitive--Green--400: hsla(142, 69%, 58%, 1);
--Primitive--Green--500: hsla(142, 71%, 45%, 1);
--Primitive--Green--600: hsla(142, 76%, 36%, 1);
--Primitive--Green--700: hsla(142, 72%, 29%, 1);
--Primitive--Green--800: hsla(143, 64%, 24%, 1);
--Primitive--Green--900: hsla(144, 61%, 20%, 1);
--Primitive--Green--950: hsla(145, 80%, 10%, 1);
--Primitive--Emerald--50: hsla(152, 81%, 96%, 1);
--Primitive--Emerald--100: hsla(149, 80%, 90%, 1);
--Primitive--Emerald--200: hsla(152, 76%, 80%, 1);
--Primitive--Emerald--300: hsla(156, 72%, 67%, 1);
--Primitive--Emerald--400: hsla(158, 64%, 52%, 1);
--Primitive--Emerald--500: hsla(160, 84%, 39%, 1);
--Primitive--Emerald--600: hsla(161, 94%, 30%, 1);
--Primitive--Emerald--700: hsla(163, 94%, 24%, 1);
--Primitive--Emerald--800: hsla(163, 88%, 20%, 1);
--Primitive--Emerald--900: hsla(164, 86%, 16%, 1);
--Primitive--Emerald--950: hsla(166, 91%, 9%, 1);
--Primitive--Teal--50: hsla(166, 76%, 97%, 1);
--Primitive--Teal--100: hsla(167, 85%, 89%, 1);
--Primitive--Teal--200: hsla(168, 84%, 78%, 1);
--Primitive--Teal--300: hsla(171, 77%, 64%, 1);
--Primitive--Teal--400: hsla(172, 66%, 50%, 1);
--Primitive--Teal--500: hsla(173, 80%, 40%, 1);
--Primitive--Teal--600: hsla(175, 84%, 32%, 1);
--Primitive--Teal--700: hsla(175, 77%, 26%, 1);
--Primitive--Teal--800: hsla(176, 69%, 22%, 1);
--Primitive--Teal--900: hsla(176, 61%, 19%, 1);
--Primitive--Teal--950: hsla(179, 84%, 10%, 1);
--Primitive--Cyan--50: hsla(183, 100%, 96%, 1);
--Primitive--Cyan--100: hsla(185, 96%, 90%, 1);
--Primitive--Cyan--200: hsla(186, 94%, 82%, 1);
--Primitive--Cyan--300: hsla(187, 92%, 69%, 1);
--Primitive--Cyan--400: hsla(188, 86%, 53%, 1);
--Primitive--Cyan--500: hsla(189, 94%, 43%, 1);
--Primitive--Cyan--600: hsla(192, 91%, 36%, 1);
--Primitive--Cyan--700: hsla(193, 82%, 31%, 1);
--Primitive--Cyan--800: hsla(194, 70%, 27%, 1);
--Primitive--Cyan--900: hsla(196, 64%, 24%, 1);
--Primitive--Cyan--950: hsla(197, 79%, 15%, 1);
--Primitive--Sky--50: hsla(204, 100%, 97%, 1);
--Primitive--Sky--100: hsla(204, 94%, 94%, 1);
--Primitive--Sky--200: hsla(201, 94%, 86%, 1);
--Primitive--Sky--300: hsla(199, 95%, 74%, 1);
--Primitive--Sky--400: hsla(198, 93%, 60%, 1);
--Primitive--Sky--500: hsla(199, 89%, 48%, 1);
--Primitive--Sky--600: hsla(200, 98%, 39%, 1);
--Primitive--Sky--700: hsla(201, 96%, 32%, 1);
--Primitive--Sky--800: hsla(201, 90%, 27%, 1);
--Primitive--Sky--900: hsla(202, 80%, 24%, 1);
--Primitive--Sky--950: hsla(204, 80%, 16%, 1);
--Primitive--Blue--50: hsla(214, 100%, 97%, 1);
--Primitive--Blue--100: hsla(214, 95%, 93%, 1);
--Primitive--Blue--200: hsla(213, 97%, 87%, 1);
--Primitive--Blue--300: hsla(212, 96%, 78%, 1);
--Primitive--Blue--400: hsla(213, 94%, 68%, 1);
--Primitive--Blue--500: hsla(217, 91%, 60%, 1);
--Primitive--Blue--600: hsla(221, 83%, 53%, 1);
--Primitive--Blue--700: hsla(224, 76%, 48%, 1);
--Primitive--Blue--800: hsla(226, 71%, 40%, 1);
--Primitive--Blue--900: hsla(224, 64%, 33%, 1);
--Primitive--Blue--950: hsla(226, 57%, 21%, 1);
--Primitive--Indigo--50: hsla(226, 100%, 97%, 1);
--Primitive--Indigo--100: hsla(226, 100%, 94%, 1);
--Primitive--Indigo--200: hsla(228, 96%, 89%, 1);
--Primitive--Indigo--300: hsla(230, 94%, 82%, 1);
--Primitive--Indigo--400: hsla(234, 89%, 74%, 1);
--Primitive--Indigo--500: hsla(239, 84%, 67%, 1);
--Primitive--Indigo--600: hsla(243, 75%, 59%, 1);
--Primitive--Indigo--700: hsla(245, 58%, 51%, 1);
--Primitive--Indigo--800: hsla(244, 55%, 41%, 1);
--Primitive--Indigo--900: hsla(242, 47%, 34%, 1);
--Primitive--Indigo--950: hsla(244, 47%, 20%, 1);
--Primitive--Violet--50: hsla(250, 100%, 98%, 1);
--Primitive--Violet--100: hsla(251, 91%, 95%, 1);
--Primitive--Violet--200: hsla(251, 95%, 92%, 1);
--Primitive--Violet--300: hsla(253, 95%, 85%, 1);
--Primitive--Violet--400: hsla(255, 92%, 76%, 1);
--Primitive--Violet--500: hsla(258, 90%, 66%, 1);
--Primitive--Violet--600: hsla(262, 83%, 58%, 1);
--Primitive--Violet--700: hsla(263, 70%, 50%, 1);
--Primitive--Violet--800: hsla(263, 69%, 42%, 1);
--Primitive--Violet--900: hsla(264, 67%, 35%, 1);
--Primitive--Violet--950: hsla(262, 78%, 23%, 1);
--Primitive--Purple--50: hsla(270, 100%, 98%, 1);
--Primitive--Purple--100: hsla(269, 100%, 95%, 1);
--Primitive--Purple--200: hsla(269, 100%, 92%, 1);
--Primitive--Purple--300: hsla(269, 97%, 85%, 1);
--Primitive--Purple--400: hsla(270, 95%, 75%, 1);
--Primitive--Purple--500: hsla(271, 91%, 65%, 1);
--Primitive--Purple--600: hsla(271, 81%, 56%, 1);
--Primitive--Purple--700: hsla(272, 72%, 47%, 1);
--Primitive--Purple--800: hsla(273, 67%, 39%, 1);
--Primitive--Purple--900: hsla(274, 66%, 32%, 1);
--Primitive--Purple--950: hsla(274, 87%, 21%, 1);
--Primitive--Fuchsia--50: hsla(289, 100%, 98%, 1);
--Primitive--Fuchsia--100: hsla(287, 100%, 95%, 1);
--Primitive--Fuchsia--200: hsla(288, 96%, 91%, 1);
--Primitive--Fuchsia--300: hsla(291, 93%, 83%, 1);
--Primitive--Fuchsia--400: hsla(292, 91%, 73%, 1);
--Primitive--Fuchsia--500: hsla(292, 84%, 61%, 1);
--Primitive--Fuchsia--600: hsla(293, 69%, 49%, 1);
--Primitive--Fuchsia--700: hsla(295, 72%, 40%, 1);
--Primitive--Fuchsia--800: hsla(295, 70%, 33%, 1);
--Primitive--Fuchsia--900: hsla(297, 64%, 28%, 1);
--Primitive--Fuchsia--950: hsla(297, 90%, 16%, 1);
--Primitive--Pink--50: hsla(327, 73%, 97%, 1);
--Primitive--Pink--100: hsla(326, 78%, 95%, 1);
--Primitive--Pink--200: hsla(326, 85%, 90%, 1);
--Primitive--Pink--300: hsla(327, 87%, 82%, 1);
--Primitive--Pink--400: hsla(329, 86%, 70%, 1);
--Primitive--Pink--500: hsla(330, 81%, 60%, 1);
--Primitive--Pink--600: hsla(333, 71%, 51%, 1);
--Primitive--Pink--700: hsla(335, 78%, 42%, 1);
--Primitive--Pink--800: hsla(336, 74%, 35%, 1);
--Primitive--Pink--900: hsla(336, 69%, 30%, 1);
--Primitive--Pink--950: hsla(336, 84%, 17%, 1);
--Primitive--Rose--50: hsla(356, 100%, 97%, 1);
--Primitive--Rose--100: hsla(356, 100%, 95%, 1);
--Primitive--Rose--200: hsla(353, 96%, 90%, 1);
--Primitive--Rose--300: hsla(353, 96%, 82%, 1);
--Primitive--Rose--400: hsla(351, 95%, 71%, 1);
--Primitive--Rose--500: hsla(350, 89%, 60%, 1);
--Primitive--Rose--600: hsla(347, 77%, 50%, 1);
--Primitive--Rose--700: hsla(345, 83%, 41%, 1);
--Primitive--Rose--800: hsla(343, 80%, 35%, 1);
--Primitive--Rose--900: hsla(342, 75%, 30%, 1);
--Primitive--Rose--950: hsla(343, 88%, 16%, 1);
--Brand--Base_Colors--Destructive: var(--Primitive--Red--500);
--Brand--Base_Colors--Success: var(--Primitive--Green--500);
--Brand--Base_Colors--Warning: var(--Primitive--Amber--500);
--Brand--Base_Colors--White: var(--Primitive--White);
--Brand--Base_Colors--Black: var(--Primitive--Black);
--Brand--Semantic_Colors--Background: var(--Primitive--Zinc--900); /*页面背景色:应用在整个页面的最底层。*/
--Brand--Semantic_Colors--Background-subtle: hsla(
0,
0%,
100%,
0.02
); /*细微背景色:用于需要与主背景有微弱区分的区域,如代码块背景。*/
--Brand--Semantic_Colors--Foreground: hsla(0, 0%, 100%, 0.9); /*主要前景/文字色:用于正文、标题等。*/
--Brand--Semantic_Colors--Foreground-secondary: hsla(0, 0%, 100%, 0.6); /*次要前景/文字色:用于辅助性文本、描述。*/
--Brand--Semantic_Colors--Foreground-muted: hsla(0, 0%, 100%, 0.4); /*静默前景/文字色:用于禁用状态的文字、占位符。*/
--Brand--Semantic_Colors--Border: hsla(0, 0%, 100%, 0.1); /*默认边框色:用于卡片、输入框、分隔线。*/
--Brand--Semantic_Colors--Border-hover: hsla(0, 0%, 100%, 0.2); /*激活边框色:用于元素被按下或激活时的边框。*/
--Brand--Semantic_Colors--Border-active: hsla(0, 0%, 100%, 0.3); /*激活边框色:用于元素被按下或激活时的边框。*/
--Brand--Semantic_Colors--Ring: hsla(
84,
81%,
44%,
0.4
); /*聚焦环颜色:用于输入框等元素在聚焦 (Focus) 状态下的外发光。*/
--Brand--UI_Element_Colors--Modal--Backdrop: hsla(0, 0%, 0%, 0.06);
--Brand--UI_Element_Colors--Modal--Thumb: hsla(0, 0%, 100%, 0.2);
--Brand--UI_Element_Colors--Modal--Thumb_Hover: hsla(0, 0%, 100%, 0.3);
--Brand--UI_Element_Colors--Icon--Default: var(--Brand--Semantic_Colors--Foreground-secondary);
--Brand--UI_Element_Colors--Icon--Hover: var(--Brand--Semantic_Colors--Foreground);
--Brand--UI_Element_Colors--Input_Select--Background: var(--Brand--Base_Colors--Black);
--Brand--UI_Element_Colors--Input_Select--Border: var(--Brand--Semantic_Colors--Border);
--Brand--UI_Element_Colors--Input_Select--Border_Hover: var(--Brand--Semantic_Colors--Border-hover);
--Brand--UI_Element_Colors--Input_Select--Border_Focus: var(--Brand--Base_Colors--Primary);
--Brand--UI_Element_Colors--Primary_Button--Background: var(--Brand--Base_Colors--Primary);
--Brand--UI_Element_Colors--Card_Container--Background: var(--Brand--Base_Colors--Black);
--Brand--UI_Element_Colors--Card_Container--Border: var(--Brand--Semantic_Colors--Border);
--Brand--UI_Element_Colors--Ghost_Button--Background: hsla(0, 0%, 100%, 0);
--Brand--UI_Element_Colors--Ghost_Button--Text: var(--Brand--Semantic_Colors--Foreground);
--Brand--UI_Element_Colors--Ghost_Button--Background_Hover: var(--Opacity--White--White-10);
--Brand--UI_Element_Colors--Ghost_Button--Background_Active: hsla(0, 0%, 100%, 0.15);
--Brand--UI_Element_Colors--Secondary_Button--Background: var(--Opacity--White--White-10);
--Brand--UI_Element_Colors--Secondary_Button--Text: var(--Brand--Semantic_Colors--Foreground);
--Brand--UI_Element_Colors--Secondary_Button--Background_Hover: var(--Opacity--White--White-20);
--Brand--UI_Element_Colors--Secondary_Button--Background_Active: hsla(0, 0%, 100%, 0.25);
--Brand--UI_Element_Colors--Secondary_Button--Border: var(--Brand--Semantic_Colors--Border);
--Brand--UI_Element_Colors--Primary_Button--Text: var(--Brand--Base_Colors--White);
--Brand--UI_Element_Colors--Primary_Button--Background_Hover: hsla(84, 81%, 44%, 0.85);
--Brand--UI_Element_Colors--Primary_Button--2nd_Background: hsla(84, 81%, 44%, 0.1);
--Brand--UI_Element_Colors--Primary_Button--3rd_Background: hsla(84, 81%, 44%, 0.05);
--Brand--UI_Element_Colors--Primary_Button--Background_Active: hsla(84, 81%, 44%, 0.7);
--Boolean: false;
}

View File

@@ -1,134 +0,0 @@
{
"name": "@cherrystudio/ui",
"version": "1.0.0-alpha.1",
"description": "Cherry Studio UI Component Library - React Components for Cherry Studio",
"main": "dist/index.js",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"react-native": "dist/index.js",
"scripts": {
"build": "tsdown",
"dev": "tsc -w",
"clean": "rm -rf dist",
"test": "vitest run",
"test:watch": "vitest",
"lint": "eslint src --ext .ts,.tsx --fix",
"type-check": "tsc --noEmit -p tsconfig.json --composite false",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build"
},
"keywords": [
"ui",
"components",
"react",
"tailwindcss",
"typescript",
"cherry-studio"
],
"author": "Cherry Studio",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/CherryHQ/cherry-studio.git"
},
"bugs": {
"url": "https://github.com/CherryHQ/cherry-studio/issues"
},
"homepage": "https://github.com/CherryHQ/cherry-studio#readme",
"peerDependencies": {
"@heroui/react": "^2.8.4",
"framer-motion": "^11.0.0 || ^12.0.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"tailwindcss": "^4.1.13"
},
"dependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^9.0.0",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-radio-group": "^1.3.8",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-use-controllable-state": "^1.2.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"lucide-react": "^0.545.0",
"react-dropzone": "^14.3.8",
"tailwind-merge": "^2.5.5"
},
"devDependencies": {
"@heroui/react": "^2.8.4",
"@storybook/addon-docs": "^9.1.6",
"@storybook/addon-themes": "^9.1.6",
"@storybook/react-vite": "^9.1.6",
"@types/react": "^19.0.12",
"@types/react-dom": "^19.0.4",
"@types/styled-components": "^5.1.34",
"@uiw/codemirror-extensions-langs": "^4.25.1",
"@uiw/codemirror-themes-all": "^4.25.1",
"@uiw/react-codemirror": "^4.25.1",
"antd": "^5.22.5",
"eslint-plugin-storybook": "9.1.6",
"framer-motion": "^12.23.12",
"linguist-languages": "^9.0.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"storybook": "^9.1.6",
"styled-components": "^6.1.15",
"tsdown": "^0.15.5",
"tsx": "^4.20.5",
"typescript": "^5.6.2",
"vitest": "^3.2.4"
},
"resolutions": {
"@codemirror/language": "6.11.3",
"@codemirror/lint": "6.8.5",
"@codemirror/view": "6.38.1"
},
"sideEffects": false,
"engines": {
"node": ">=18.0.0"
},
"files": [
"dist",
"README.md"
],
"exports": {
".": {
"types": "./dist/index.d.ts",
"react-native": "./dist/index.js",
"import": "./dist/index.mjs",
"require": "./dist/index.js",
"default": "./dist/index.js"
},
"./components": {
"types": "./dist/components/index.d.ts",
"react-native": "./dist/components/index.js",
"import": "./dist/components/index.mjs",
"require": "./dist/components/index.js",
"default": "./dist/components/index.js"
},
"./hooks": {
"types": "./dist/hooks/index.d.ts",
"react-native": "./dist/hooks/index.js",
"import": "./dist/hooks/index.mjs",
"require": "./dist/hooks/index.js",
"default": "./dist/hooks/index.js"
},
"./utils": {
"types": "./dist/utils/index.d.ts",
"react-native": "./dist/utils/index.js",
"import": "./dist/utils/index.mjs",
"require": "./dist/utils/index.js",
"default": "./dist/utils/index.js"
},
"./styles": "./src/styles/index.css",
"./styles/tokens.css": "./src/styles/tokens.css",
"./styles/theme.css": "./src/styles/theme.css",
"./styles/index.css": "./src/styles/index.css"
},
"packageManager": "yarn@4.9.1"
}

View File

@@ -1,139 +0,0 @@
import type { BasicSetupOptions } from '@uiw/react-codemirror'
import CodeMirror, { Annotation, EditorView } from '@uiw/react-codemirror'
import { useCallback, useEffect, useImperativeHandle, useMemo, useRef } from 'react'
import { memo } from 'react'
import { useBlurHandler, useHeightListener, useLanguageExtensions, useSaveKeymap } from './hooks'
import type { CodeEditorProps } from './types'
import { prepareCodeChanges } from './utils'
/**
* A code editor component based on CodeMirror.
* This is a wrapper of ReactCodeMirror.
*/
const CodeEditor = ({
ref,
value,
placeholder,
language,
languageConfig,
onSave,
onChange,
onBlur,
onHeightChange,
height,
maxHeight,
minHeight,
options,
extensions,
theme = 'light',
fontSize = 16,
style,
className,
editable = true,
readOnly = false,
expanded = true,
wrapped = true
}: CodeEditorProps) => {
const basicSetup = useMemo(() => {
return {
dropCursor: true,
allowMultipleSelections: true,
indentOnInput: true,
bracketMatching: true,
closeBrackets: true,
rectangularSelection: true,
crosshairCursor: true,
highlightActiveLineGutter: false,
highlightSelectionMatches: true,
closeBracketsKeymap: options?.keymap,
searchKeymap: options?.keymap,
foldKeymap: options?.keymap,
completionKeymap: options?.keymap,
lintKeymap: options?.keymap,
...(options as BasicSetupOptions)
}
}, [options])
const initialContent = useRef(options?.stream ? (value ?? '').trimEnd() : (value ?? ''))
const editorViewRef = useRef<EditorView | null>(null)
const langExtensions = useLanguageExtensions(language, options?.lint, languageConfig)
const handleSave = useCallback(() => {
const currentDoc = editorViewRef.current?.state.doc.toString() ?? ''
onSave?.(currentDoc)
}, [onSave])
// Calculate changes during streaming response to update EditorView
// Cannot handle user editing code during streaming response (and probably doesn't need to)
useEffect(() => {
if (!editorViewRef.current) return
const newContent = options?.stream ? (value ?? '').trimEnd() : (value ?? '')
const currentDoc = editorViewRef.current.state.doc.toString()
const changes = prepareCodeChanges(currentDoc, newContent)
if (changes && changes.length > 0) {
editorViewRef.current.dispatch({
changes,
annotations: [Annotation.define<boolean>().of(true)]
})
}
}, [options?.stream, value])
const saveKeymapExtension = useSaveKeymap({ onSave, enabled: options?.keymap })
const blurExtension = useBlurHandler({ onBlur })
const heightListenerExtension = useHeightListener({ onHeightChange })
const customExtensions = useMemo(() => {
return [
...(extensions ?? []),
...langExtensions,
...(wrapped ? [EditorView.lineWrapping] : []),
saveKeymapExtension,
blurExtension,
heightListenerExtension
].flat()
}, [extensions, langExtensions, wrapped, saveKeymapExtension, blurExtension, heightListenerExtension])
useImperativeHandle(ref, () => ({
save: handleSave
}))
return (
<CodeMirror
// Set to a stable value to avoid triggering CodeMirror reset
value={initialContent.current}
placeholder={placeholder}
width="100%"
height={expanded ? undefined : height}
maxHeight={expanded ? undefined : maxHeight}
minHeight={minHeight}
editable={editable}
readOnly={readOnly}
theme={theme}
extensions={customExtensions}
onCreateEditor={(view: EditorView) => {
editorViewRef.current = view
onHeightChange?.(view.scrollDOM?.scrollHeight ?? 0)
}}
onChange={(value, viewUpdate) => {
if (onChange && viewUpdate.docChanged) onChange(value)
}}
basicSetup={basicSetup}
style={{
fontSize,
marginTop: 0,
borderRadius: 'inherit',
...style
}}
className={`code-editor ${className ?? ''}`}
/>
)
}
CodeEditor.displayName = 'CodeEditor'
export default memo(CodeEditor)

View File

@@ -1,41 +0,0 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { getNormalizedExtension } from '../utils'
const hoisted = vi.hoisted(() => ({
languages: {
svg: { extensions: ['.svg'] },
TypeScript: { extensions: ['.ts'] }
}
}))
vi.mock('@shared/config/languages', () => ({
languages: hoisted.languages
}))
describe('getNormalizedExtension', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('should return custom mapping for custom language', async () => {
await expect(getNormalizedExtension('svg')).resolves.toBe('xml')
await expect(getNormalizedExtension('SVG')).resolves.toBe('xml')
})
it('should prefer custom mapping when both custom and linguist exist', async () => {
await expect(getNormalizedExtension('svg')).resolves.toBe('xml')
})
it('should return linguist mapping when available (strip leading dot)', async () => {
await expect(getNormalizedExtension('TypeScript')).resolves.toBe('ts')
})
it('should return extension when input already looks like extension (leading dot)', async () => {
await expect(getNormalizedExtension('.json')).resolves.toBe('json')
})
it('should return language as-is when no rules matched', async () => {
await expect(getNormalizedExtension('unknownLanguage')).resolves.toBe('unknownLanguage')
})
})

View File

@@ -1,204 +0,0 @@
import { linter } from '@codemirror/lint' // statically imported by @uiw/codemirror-extensions-basic-setup
import { EditorView } from '@codemirror/view'
import type { Extension } from '@uiw/react-codemirror'
import { keymap } from '@uiw/react-codemirror'
import { useEffect, useMemo, useState } from 'react'
import type { LanguageConfig } from './types'
import { getNormalizedExtension } from './utils'
/** 语言对应的 linter 加载器
* key: 语言文件扩展名(不包含 `.`
*/
const linterLoaders: Record<string, () => Promise<any>> = {
json: async () => {
const jsonParseLinter = await import('@codemirror/lang-json').then((mod) => mod.jsonParseLinter)
return linter(jsonParseLinter())
}
}
/**
* 特殊语言加载器
* key: 语言文件扩展名(不包含 `.`
*/
const specialLanguageLoaders: Record<string, () => Promise<Extension>> = {
dot: async () => {
const mod = await import('@viz-js/lang-dot')
return mod.dot()
},
// @uiw/codemirror-extensions-langs 4.25.1 移除了 mermaid 支持,这里加回来
mmd: async () => {
const mod = await import('codemirror-lang-mermaid')
return mod.mermaid()
}
}
/**
* 加载语言扩展
*/
async function loadLanguageExtension(language: string, languageConfig?: LanguageConfig): Promise<Extension | null> {
const fileExt = await getNormalizedExtension(language, languageConfig)
// 尝试加载特殊语言
const specialLoader = specialLanguageLoaders[fileExt]
if (specialLoader) {
try {
return await specialLoader()
} catch (error) {
console.debug(`Failed to load language ${language} (${fileExt})`, error as Error)
return null
}
}
// 回退到 uiw/codemirror 包含的语言
try {
const { loadLanguage } = await import('@uiw/codemirror-extensions-langs')
const extension = loadLanguage(fileExt as any)
return extension || null
} catch (error) {
console.debug(`Failed to load language ${language} (${fileExt})`, error as Error)
return null
}
}
/**
* 加载 linter 扩展
*/
async function loadLinterExtension(language: string, languageConfig?: LanguageConfig): Promise<Extension | null> {
const fileExt = await getNormalizedExtension(language, languageConfig)
const loader = linterLoaders[fileExt]
if (!loader) return null
try {
return await loader()
} catch (error) {
console.debug(`Failed to load linter for ${language} (${fileExt})`, error as Error)
return null
}
}
/**
* 加载语言相关扩展
*/
export const useLanguageExtensions = (language: string, lint?: boolean, languageConfig?: LanguageConfig) => {
const [extensions, setExtensions] = useState<Extension[]>([])
useEffect(() => {
let cancelled = false
const loadAllExtensions = async () => {
try {
// 加载所有扩展
const [languageResult, linterResult] = await Promise.allSettled([
loadLanguageExtension(language, languageConfig),
lint ? loadLinterExtension(language, languageConfig) : Promise.resolve(null)
])
if (cancelled) return
const results: Extension[] = []
// 语言扩展
if (languageResult.status === 'fulfilled' && languageResult.value) {
results.push(languageResult.value)
}
// linter 扩展
if (linterResult.status === 'fulfilled' && linterResult.value) {
results.push(linterResult.value)
}
setExtensions(results)
} catch (error) {
if (!cancelled) {
console.debug('Failed to load language extensions:', error as Error)
setExtensions([])
}
}
}
loadAllExtensions()
return () => {
cancelled = true
}
}, [language, lint, languageConfig])
return extensions
}
interface UseSaveKeymapProps {
onSave?: (content: string) => void
enabled?: boolean
}
/**
* CodeMirror 扩展,用于处理保存快捷键 (Cmd/Ctrl + S)
* @param onSave 保存时触发的回调函数
* @param enabled 是否启用此快捷键
* @returns 扩展或空数组
*/
export function useSaveKeymap({ onSave, enabled = true }: UseSaveKeymapProps) {
return useMemo(() => {
if (!enabled || !onSave) {
return []
}
return keymap.of([
{
key: 'Mod-s',
run: (view: EditorView) => {
onSave(view.state.doc.toString())
return true
},
preventDefault: true
}
])
}, [onSave, enabled])
}
interface UseBlurHandlerProps {
onBlur?: (content: string) => void
}
/**
* CodeMirror 扩展,用于处理编辑器的 blur 事件
* @param onBlur blur 事件触发时的回调函数
* @returns 扩展或空数组
*/
export function useBlurHandler({ onBlur }: UseBlurHandlerProps) {
return useMemo(() => {
if (!onBlur) {
return []
}
return EditorView.domEventHandlers({
blur: (_event, view) => {
onBlur(view.state.doc.toString())
}
})
}, [onBlur])
}
interface UseHeightListenerProps {
onHeightChange?: (scrollHeight: number) => void
}
/**
* CodeMirror 扩展,用于监听编辑器高度变化
* @param onHeightChange 高度变化时触发的回调函数
* @returns 扩展或空数组
*/
export function useHeightListener({ onHeightChange }: UseHeightListenerProps) {
return useMemo(() => {
if (!onHeightChange) {
return []
}
return EditorView.updateListener.of((update) => {
if (update.docChanged || update.heightChanged) {
onHeightChange(update.view.scrollDOM?.scrollHeight ?? 0)
}
})
}, [onHeightChange])
}

View File

@@ -1,3 +0,0 @@
export { default } from './CodeEditor'
export * from './types'
export { getCmThemeByName, getCmThemeNames } from './utils'

View File

@@ -1,114 +0,0 @@
import type { BasicSetupOptions, Extension } from '@uiw/react-codemirror'
export type CodeMirrorTheme = 'light' | 'dark' | 'none' | Extension
/** Language data structure for file extension mapping */
export interface LanguageData {
type: string
aliases?: string[]
extensions?: string[]
}
/** Language configuration mapping language names to their data */
export type LanguageConfig = Record<string, LanguageData>
export interface CodeEditorHandles {
save?: () => void
}
export interface CodeEditorProps {
ref?: React.RefObject<CodeEditorHandles | null>
/** Value used in controlled mode, e.g., code blocks. */
value: string
/** Placeholder when the editor content is empty. */
placeholder?: string | HTMLElement
/**
* Code language string.
* - Case-insensitive.
* - Supports common names: javascript, json, python, etc.
* - Supports aliases: c#/csharp, objective-c++/obj-c++/objc++, etc.
* - Supports file extensions: .cpp/cpp, .js/js, .py/py, etc.
*/
language: string
/**
* Language configuration for extension mapping.
* If not provided, will use a default minimal configuration.
* @optional
*/
languageConfig?: LanguageConfig
/** Fired when ref.save() is called or the save shortcut is triggered. */
onSave?: (newContent: string) => void
/** Fired when the editor content changes. */
onChange?: (newContent: string) => void
/** Fired when the editor loses focus. */
onBlur?: (newContent: string) => void
/** Fired when the editor height changes. */
onHeightChange?: (scrollHeight: number) => void
/**
* Fixed editor height, not exceeding maxHeight.
* Only works when expanded is false.
*/
height?: string
/**
* Maximum editor height.
* Only works when expanded is false.
*/
maxHeight?: string
/** Minimum editor height. */
minHeight?: string
/** Editor options that extend BasicSetupOptions. */
options?: {
/**
* Whether to enable special treatment for stream response.
* @default false
*/
stream?: boolean
/**
* Whether to enable linting.
* @default false
*/
lint?: boolean
/**
* Whether to enable keymap.
* @default false
*/
keymap?: boolean
} & BasicSetupOptions
/** Additional extensions for CodeMirror. */
extensions?: Extension[]
/**
* CodeMirror theme name: 'light', 'dark', 'none', Extension.
* @default 'light'
*/
theme?: CodeMirrorTheme
/**
* Font size that overrides the app setting.
* @default 16
*/
fontSize?: number
/** Style overrides for the editor, passed directly to CodeMirror's style property. */
style?: React.CSSProperties
/** CSS class name appended to the default `code-editor` class. */
className?: string
/**
* Whether the editor view is editable.
* @default true
*/
editable?: boolean
/**
* Set the editor state to read only but keep some user interactions, e.g., keymaps.
* @default false
*/
readOnly?: boolean
/**
* Whether the editor is expanded.
* If true, the height and maxHeight props are ignored.
* @default true
*/
expanded?: boolean
/**
* Whether the code lines are wrapped.
* @default true
*/
wrapped?: boolean
}

View File

@@ -1,268 +0,0 @@
import * as cmThemes from '@uiw/codemirror-themes-all'
import type { Extension } from '@uiw/react-codemirror'
import diff from 'fast-diff'
import type { CodeMirrorTheme, LanguageConfig } from './types'
/**
* Computes code changes using fast-diff and converts them to CodeMirror changes.
* Could handle all types of changes, though insertions are most common during streaming responses.
* @param oldCode The old code content
* @param newCode The new code content
* @returns An array of changes for EditorView.dispatch
*/
export function prepareCodeChanges(oldCode: string, newCode: string) {
const diffResult = diff(oldCode, newCode)
const changes: { from: number; to: number; insert: string }[] = []
let offset = 0
// operation: 1=insert, -1=delete, 0=equal
for (const [operation, text] of diffResult) {
if (operation === 1) {
changes.push({
from: offset,
to: offset,
insert: text
})
} else if (operation === -1) {
changes.push({
from: offset,
to: offset + text.length,
insert: ''
})
offset += text.length
} else {
offset += text.length
}
}
return changes
}
// Custom language file extension mapping
// key: language name in lowercase
// value: file extension
const _customLanguageExtensions: Record<string, string> = {
svg: 'xml',
vab: 'vb',
graphviz: 'dot'
}
// Default minimal language configuration for common languages
const _defaultLanguageConfig: LanguageConfig = {
JavaScript: {
type: 'programming',
extensions: ['.js', '.mjs', '.cjs'],
aliases: ['js', 'node']
},
TypeScript: {
type: 'programming',
extensions: ['.ts'],
aliases: ['ts']
},
Python: {
type: 'programming',
extensions: ['.py'],
aliases: ['python3', 'py']
},
Java: {
type: 'programming',
extensions: ['.java']
},
'C++': {
type: 'programming',
extensions: ['.cpp', '.cc', '.cxx'],
aliases: ['cpp']
},
C: {
type: 'programming',
extensions: ['.c']
},
'C#': {
type: 'programming',
extensions: ['.cs'],
aliases: ['csharp']
},
HTML: {
type: 'markup',
extensions: ['.html', '.htm']
},
CSS: {
type: 'markup',
extensions: ['.css']
},
JSON: {
type: 'data',
extensions: ['.json']
},
XML: {
type: 'data',
extensions: ['.xml']
},
YAML: {
type: 'data',
extensions: ['.yml', '.yaml']
},
SQL: {
type: 'data',
extensions: ['.sql']
},
Shell: {
type: 'programming',
extensions: ['.sh', '.bash'],
aliases: ['bash', 'sh']
},
Go: {
type: 'programming',
extensions: ['.go'],
aliases: ['golang']
},
Rust: {
type: 'programming',
extensions: ['.rs']
},
PHP: {
type: 'programming',
extensions: ['.php']
},
Ruby: {
type: 'programming',
extensions: ['.rb'],
aliases: ['rb']
},
Swift: {
type: 'programming',
extensions: ['.swift']
},
Kotlin: {
type: 'programming',
extensions: ['.kt']
},
Dart: {
type: 'programming',
extensions: ['.dart']
},
R: {
type: 'programming',
extensions: ['.r']
},
MATLAB: {
type: 'programming',
extensions: ['.m']
}
}
/**
* Get the file extension of the language, by language name
* - First, exact match
* - Then, case-insensitive match
* - Finally, match aliases
* If there are multiple file extensions, only the first one will be returned
* @param language language name
* @param languageConfig optional language configuration, defaults to a minimal config
* @returns file extension
*/
export function getExtensionByLanguage(language: string, languageConfig?: LanguageConfig): string {
const languages = languageConfig || _defaultLanguageConfig
const lowerLanguage = language.toLowerCase()
// Exact match language name
const directMatch = languages[language]
if (directMatch?.extensions?.[0]) {
return directMatch.extensions[0]
}
// Case-insensitive match language name
for (const [langName, data] of Object.entries(languages)) {
if (langName.toLowerCase() === lowerLanguage && data.extensions?.[0]) {
return data.extensions[0]
}
}
// Match aliases
for (const [, data] of Object.entries(languages)) {
if (data.aliases?.some((alias) => alias.toLowerCase() === lowerLanguage)) {
return data.extensions?.[0] || `.${language}`
}
}
// Fallback to language name
return `.${language}`
}
/**
* Get the file extension of the language, for @uiw/codemirror-extensions-langs
* - First, search for custom extensions
* - Then, search for language configuration extensions
* - Finally, assume the name is already an extension
* @param language language name
* @param languageConfig optional language configuration
* @returns file extension (without `.` prefix)
*/
export async function getNormalizedExtension(language: string, languageConfig?: LanguageConfig) {
let lang = language
// If the language name looks like an extension, remove the dot
if (language.startsWith('.') && language.length > 1) {
lang = language.slice(1)
}
const lowerLanguage = lang.toLowerCase()
// 1. Search for custom extensions
const customExt = _customLanguageExtensions[lowerLanguage]
if (customExt) {
return customExt
}
// 2. Search for language configuration extensions
const linguistExt = getExtensionByLanguage(lang, languageConfig)
if (linguistExt) {
return linguistExt.slice(1)
}
// Fallback to language name
return lang
}
/**
* Get the list of CodeMirror theme names
* - Include auto, light, dark
* - Include all themes in @uiw/codemirror-themes-all
*
* A more robust approach might be to hardcode the theme list
* @returns theme name list
*/
export function getCmThemeNames(): string[] {
return ['auto', 'light', 'dark']
.concat(Object.keys(cmThemes))
.filter((item) => typeof (cmThemes as any)[item] !== 'function')
.filter((item) => !/^(defaultSettings)/.test(item as string) && !/(Style)$/.test(item as string))
}
/**
* Get the CodeMirror theme object by theme name
* @param name theme name
* @returns theme object
*/
export function getCmThemeByName(name: string): CodeMirrorTheme {
// 1. Search for the extension of the corresponding theme in @uiw/codemirror-themes-all
const candidate = (cmThemes as Record<string, unknown>)[name]
if (
Object.prototype.hasOwnProperty.call(cmThemes, name) &&
typeof candidate !== 'function' &&
!/^defaultSettings/i.test(name) &&
!/(Style)$/.test(name)
) {
return candidate as Extension
}
// 2. Basic string theme
if (name === 'light' || name === 'dark' || name === 'none') {
return name
}
// 3. If not found, fallback to light
return 'light'
}

View File

@@ -1,106 +0,0 @@
// Original path: src/renderer/src/components/CollapsibleSearchBar.tsx
import type { InputRef } from 'antd'
import { Input } from 'antd'
import { Search } from 'lucide-react'
import { motion } from 'motion/react'
import React, { memo, useCallback, useEffect, useRef, useState } from 'react'
import { Tooltip } from '../../primitives/tooltip'
interface CollapsibleSearchBarProps {
onSearch: (text: string) => void
placeholder?: string
tooltip?: string
icon?: React.ReactNode
maxWidth?: string | number
style?: React.CSSProperties
}
/**
* A collapsible search bar for list headers
* Renders as an icon initially, expands to full search input when clicked
*/
const CollapsibleSearchBar = ({
onSearch,
placeholder = 'Search',
tooltip = 'Search',
icon = <Search size={14} color="var(--color-icon)" />,
maxWidth = '100%',
style
}: CollapsibleSearchBarProps) => {
const [searchVisible, setSearchVisible] = useState(false)
const [searchText, setSearchText] = useState('')
const inputRef = useRef<InputRef>(null)
const handleTextChange = useCallback(
(text: string) => {
setSearchText(text)
onSearch(text)
},
[onSearch]
)
const handleClear = useCallback(() => {
setSearchText('')
setSearchVisible(false)
onSearch('')
}, [onSearch])
useEffect(() => {
if (searchVisible && inputRef.current) {
inputRef.current.focus()
}
}, [searchVisible])
return (
<div style={{ display: 'flex', alignItems: 'center', position: 'relative' }}>
<motion.div
initial="collapsed"
animate={searchVisible ? 'expanded' : 'collapsed'}
variants={{
expanded: { maxWidth: maxWidth, opacity: 1, transition: { duration: 0.3, ease: 'easeInOut' } },
collapsed: { maxWidth: 0, opacity: 0, transition: { duration: 0.3, ease: 'easeInOut' } }
}}
style={{ overflow: 'hidden', flex: 1 }}>
<Input
ref={inputRef}
type="text"
placeholder={placeholder}
size="small"
suffix={icon}
value={searchText}
autoFocus
allowClear
onChange={(e) => handleTextChange(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Escape') {
e.stopPropagation()
handleTextChange('')
if (!searchText) setSearchVisible(false)
}
}}
onBlur={() => {
if (!searchText) setSearchVisible(false)
}}
onClear={handleClear}
style={{ width: '100%', ...style }}
/>
</motion.div>
<motion.div
initial="visible"
animate={searchVisible ? 'hidden' : 'visible'}
variants={{
visible: { opacity: 1, transition: { duration: 0.1, delay: 0.3, ease: 'easeInOut' } },
hidden: { opacity: 0, transition: { duration: 0.1, ease: 'easeInOut' } }
}}
style={{ cursor: 'pointer', display: 'flex' }}
onClick={() => setSearchVisible(true)}>
<Tooltip content={tooltip} delay={500} closeDelay={0}>
{icon}
</Tooltip>
</motion.div>
</div>
)
}
export default memo(CollapsibleSearchBar)

View File

@@ -1,8 +0,0 @@
// Original path: src/renderer/src/components/DraggableList/index.tsx
export { default as DraggableList } from './list'
export { useDraggableReorder } from './useDraggableReorder'
export {
default as DraggableVirtualList,
type DraggableVirtualListProps,
type DraggableVirtualListRef
} from './virtual-list'

View File

@@ -1,109 +0,0 @@
// Original path: src/renderer/src/components/DraggableList/list.tsx
import type {
DroppableProps,
DropResult,
OnDragEndResponder,
OnDragStartResponder,
ResponderProvided
} from '@hello-pangea/dnd'
import { DragDropContext, Draggable, Droppable } from '@hello-pangea/dnd'
import type { HTMLAttributes, Key } from 'react'
import { useCallback } from 'react'
// Inline utility function from @renderer/utils
function droppableReorder<T>(list: T[], sourceIndex: number, destIndex: number, len: number = 1): T[] {
const result = Array.from(list)
const removed = result.splice(sourceIndex, len)
if (sourceIndex < destIndex) {
result.splice(destIndex - len + 1, 0, ...removed)
} else {
result.splice(destIndex, 0, ...removed)
}
return result
}
interface Props<T> {
list: T[]
style?: React.CSSProperties
listStyle?: React.CSSProperties
listProps?: HTMLAttributes<HTMLDivElement>
children: (item: T, index: number) => React.ReactNode
itemKey?: keyof T | ((item: T) => Key)
onUpdate: (list: T[]) => void
onDragStart?: OnDragStartResponder
onDragEnd?: OnDragEndResponder
droppableProps?: Partial<DroppableProps>
}
function DraggableList<T>({
children,
list,
style,
listStyle,
listProps,
itemKey,
droppableProps,
onDragStart,
onUpdate,
onDragEnd
}: Props<T>) {
const _onDragEnd = (result: DropResult, provided: ResponderProvided) => {
onDragEnd?.(result, provided)
if (result.destination) {
const sourceIndex = result.source.index
const destIndex = result.destination.index
if (sourceIndex !== destIndex) {
const reorderAgents = droppableReorder(list, sourceIndex, destIndex)
onUpdate(reorderAgents)
}
}
}
const getId = useCallback(
(item: T) => {
if (typeof itemKey === 'function') return itemKey(item)
if (itemKey) return item[itemKey] as Key
if (typeof item === 'string') return item as Key
if (item && typeof item === 'object' && 'id' in item) return item.id as Key
return undefined
},
[itemKey]
)
return (
<DragDropContext onDragStart={onDragStart} onDragEnd={_onDragEnd}>
<Droppable droppableId="droppable" {...droppableProps}>
{(provided) => (
<div {...provided.droppableProps} ref={provided.innerRef} style={style}>
<div {...listProps} className="draggable-list-container">
{list.map((item, index) => {
const draggableId = String(getId(item) ?? index)
return (
<Draggable key={`draggable_${draggableId}`} draggableId={draggableId} index={index}>
{(provided) => (
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
style={{
...listStyle,
...provided.draggableProps.style,
marginBottom: 8
}}>
{children(item, index)}
</div>
)}
</Draggable>
)
})}
</div>
{provided.placeholder}
</div>
)}
</Droppable>
</DragDropContext>
)
}
export default DraggableList

View File

@@ -1,20 +0,0 @@
/**
* 用于 dnd 列表的元素重新排序方法。支持多元素"拖动"排序。
* @template {T} 列表元素的类型
* @param {T[]} list 要重新排序的列表
* @param {number} sourceIndex 起始元素索引
* @param {number} destIndex 目标元素索引
* @param {number} [len=1] 要移动的元素数量,默认为 1
* @returns {T[]} 重新排序后的列表
*/
export function droppableReorder<T>(list: T[], sourceIndex: number, destIndex: number, len: number = 1): T[] {
const result = Array.from(list)
const removed = result.splice(sourceIndex, len)
if (sourceIndex < destIndex) {
result.splice(destIndex - len + 1, 0, ...removed)
} else {
result.splice(destIndex, 0, ...removed)
}
return result
}

View File

@@ -1,80 +0,0 @@
// Original path: src/renderer/src/components/DraggableList/useDraggableReorder.ts
import type { DropResult } from '@hello-pangea/dnd'
import type { Key } from 'react'
import { useCallback, useMemo } from 'react'
interface UseDraggableReorderParams<T> {
/** 原始的、完整的数据列表 */
originalList: T[]
/** 当前在界面上渲染的、可能被过滤的列表 */
filteredList: T[]
/** 用于更新原始列表状态的函数 */
onUpdate: (newList: T[]) => void
/** 用于从列表项中获取唯一ID的属性名或函数 */
itemKey: keyof T | ((item: T) => Key)
}
/**
* 增强拖拽排序能力,处理"过滤后列表"与"原始列表"的索引映射问题。
*
* @template T 列表项的类型
* @param params - { originalList, filteredList, onUpdate, idKey }
* @returns 返回可以直接传递给 DraggableVirtualList 的 props: { onDragEnd, itemKey }
*/
export function useDraggableReorder<T>({
originalList,
filteredList,
onUpdate,
itemKey
}: UseDraggableReorderParams<T>) {
const getId = useCallback(
(item: T) => (typeof itemKey === 'function' ? itemKey(item) : (item[itemKey] as Key)),
[itemKey]
)
// 创建从 item ID 到其在 *原始列表* 中索引的映射
const itemIndexMap = useMemo(() => {
const map = new Map<Key, number>()
originalList.forEach((item, index) => {
map.set(getId(item), index)
})
return map
}, [originalList, getId])
// 创建一个函数,将 *过滤后列表* 的视图索引转换为 *原始列表* 的数据索引
const getItemKey = useCallback(
(index: number): Key => {
const item = filteredList[index]
// 如果找不到item返回视图索引兜底
if (!item) return index
const originalIndex = itemIndexMap.get(getId(item))
return originalIndex ?? index
},
[filteredList, itemIndexMap, getId]
)
// 创建 onDragEnd 回调,封装了所有重排逻辑
const onDragEnd = useCallback(
(result: DropResult) => {
if (!result.destination) return
// 使用 getItemKey 将视图索引转换为数据索引
const sourceOriginalIndex = getItemKey(result.source.index) as number
const destOriginalIndex = getItemKey(result.destination.index) as number
if (sourceOriginalIndex === destOriginalIndex) return
// 操作原始列表的副本
const newList = [...originalList]
const [movedItem] = newList.splice(sourceOriginalIndex, 1)
newList.splice(destOriginalIndex, 0, movedItem)
// 调用外部更新函数
onUpdate(newList)
},
[originalList, onUpdate, getItemKey]
)
return { onDragEnd, itemKey: getItemKey }
}

View File

@@ -1,256 +0,0 @@
import type {
DroppableProps,
DropResult,
OnDragEndResponder,
OnDragStartResponder,
ResponderProvided
} from '@hello-pangea/dnd'
import { DragDropContext, Draggable, Droppable } from '@hello-pangea/dnd'
import { type ScrollToOptions, useVirtualizer, type VirtualItem } from '@tanstack/react-virtual'
import { type Key, memo, useCallback, useImperativeHandle, useRef } from 'react'
import Scrollbar from '../Scrollbar'
import { droppableReorder } from './sort'
export interface DraggableVirtualListRef {
measure: () => void
scrollElement: () => HTMLDivElement | null
scrollToOffset: (offset: number, options?: ScrollToOptions) => void
scrollToIndex: (index: number, options?: ScrollToOptions) => void
resizeItem: (index: number, size: number) => void
getTotalSize: () => number
getVirtualItems: () => VirtualItem[]
getVirtualIndexes: () => number[]
}
/**
* 泛型 Props用于配置 DraggableVirtualList。
*
* @template T 列表元素的类型
* @property {string} [className] 根节点附加 class
* @property {React.CSSProperties} [style] 根节点附加样式
* @property {React.CSSProperties} [itemStyle] 元素内容区域的附加样式
* @property {React.CSSProperties} [itemContainerStyle] 元素拖拽容器的附加样式
* @property {Partial<DroppableProps>} [droppableProps] 透传给 Droppable 的额外配置
* @property {(list: T[]) => void} [onUpdate] 拖拽排序完成后的回调,返回新的列表顺序(可被 useDraggableReorder 替代)
* @property {OnDragStartResponder} [onDragStart] 开始拖拽时的回调
* @property {OnDragEndResponder} [onDragEnd] 结束拖拽时的回调
* @property {T[]} list 渲染的数据源
* @property {(index: number) => Key} [itemKey] 提供给虚拟列表的行 key若不提供默认使用 index
* @property {number} [overscan=5] 前后额外渲染的行数,提升快速滚动时的体验
* @property {React.ReactNode} [header] 列表头部内容
* @property {(item: T, index: number) => React.ReactNode} children 列表项渲染函数
*/
export interface DraggableVirtualListProps<T> {
ref?: React.Ref<DraggableVirtualListRef>
className?: string
style?: React.CSSProperties
scrollerStyle?: React.CSSProperties
itemStyle?: React.CSSProperties
itemContainerStyle?: React.CSSProperties
droppableProps?: Partial<DroppableProps>
onUpdate?: (list: T[]) => void
onDragStart?: OnDragStartResponder
onDragEnd?: OnDragEndResponder
list: T[]
itemKey?: (index: number) => Key
estimateSize?: (index: number) => number
overscan?: number
header?: React.ReactNode
children: (item: T, index: number) => React.ReactNode
disabled?: boolean
}
/**
* 带虚拟滚动与拖拽排序能力的(垂直)列表组件。
* - 滚动容器由该组件内部管理。
* @template T 列表元素的类型
* @param {DraggableVirtualListProps<T>} props 组件参数
* @returns {React.ReactElement}
*/
function DraggableVirtualList<T>({
ref,
className,
style,
scrollerStyle,
itemStyle,
itemContainerStyle,
droppableProps,
onDragStart,
onUpdate,
onDragEnd,
list,
itemKey,
estimateSize: _estimateSize,
overscan = 5,
header,
children,
disabled
}: DraggableVirtualListProps<T>): React.ReactElement {
const _onDragEnd = (result: DropResult, provided: ResponderProvided) => {
onDragEnd?.(result, provided)
if (onUpdate && result.destination) {
const sourceIndex = result.source.index
const destIndex = result.destination.index
if (sourceIndex !== destIndex) {
const reorderAgents = droppableReorder(list, sourceIndex, destIndex)
onUpdate(reorderAgents)
}
}
}
// 虚拟列表滚动容器的 ref
const parentRef = useRef<HTMLDivElement>(null)
const virtualizer = useVirtualizer({
count: list?.length ?? 0,
getScrollElement: useCallback(() => parentRef.current, []),
getItemKey: itemKey,
estimateSize: useCallback((index) => _estimateSize?.(index) ?? 50, [_estimateSize]),
overscan
})
useImperativeHandle(
ref,
() => ({
measure: () => virtualizer.measure(),
scrollElement: () => virtualizer.scrollElement,
scrollToOffset: (offset, options) => virtualizer.scrollToOffset(offset, options),
scrollToIndex: (index, options) => virtualizer.scrollToIndex(index, options),
resizeItem: (index, size) => virtualizer.resizeItem(index, size),
getTotalSize: () => virtualizer.getTotalSize(),
getVirtualItems: () => virtualizer.getVirtualItems(),
getVirtualIndexes: () => virtualizer.getVirtualItems().map((item) => item.index)
}),
[virtualizer]
)
return (
<div
className={`${className} draggable-virtual-list`}
style={{ height: '100%', display: 'flex', flexDirection: 'column', ...style }}>
<DragDropContext onDragStart={onDragStart} onDragEnd={_onDragEnd}>
{header}
<Droppable
droppableId="droppable"
mode="virtual"
renderClone={(provided, _snapshot, rubric) => {
const item = list[rubric.source.index]
return (
<div
{...provided.draggableProps}
{...provided.dragHandleProps}
ref={provided.innerRef}
style={{
...itemStyle,
...provided.draggableProps.style
}}>
{item && children(item, rubric.source.index)}
</div>
)
}}
{...droppableProps}>
{(provided) => {
// 让 dnd 和虚拟列表共享同一个滚动容器
const setRefs = (el: HTMLDivElement | null) => {
provided.innerRef(el)
parentRef.current = el
}
return (
<Scrollbar
ref={setRefs}
{...provided.droppableProps}
className="virtual-scroller"
style={{
...scrollerStyle,
height: '100%',
width: '100%',
overflowY: 'auto',
position: 'relative'
}}>
<div
className="virtual-list"
style={{
height: `${virtualizer.getTotalSize()}px`,
width: '100%',
position: 'relative'
}}>
{virtualizer.getVirtualItems().map((virtualItem) => (
<VirtualRow
key={virtualItem.key}
virtualItem={virtualItem}
list={list}
itemStyle={itemStyle}
itemContainerStyle={itemContainerStyle}
virtualizer={virtualizer}
children={children}
disabled={disabled}
/>
))}
</div>
</Scrollbar>
)
}}
</Droppable>
</DragDropContext>
</div>
)
}
/**
* 渲染单个可拖拽的虚拟列表项,高度为动态测量
*/
const VirtualRow = memo(
({ virtualItem, list, children, itemStyle, itemContainerStyle, virtualizer, disabled }: any) => {
const item = list[virtualItem.index]
const draggableId = String(virtualItem.key)
return (
<Draggable
key={`draggable_${draggableId}`}
draggableId={draggableId}
isDragDisabled={disabled}
index={virtualItem.index}>
{(provided) => {
const setDragRefs = (el: HTMLElement | null) => {
provided.innerRef(el)
virtualizer.measureElement(el)
}
const dndStyle = provided.draggableProps.style
const virtualizerTransform = `translateY(${virtualItem.start}px)`
// dnd 的 transform 负责拖拽时的位移和让位动画,
// virtualizer 的 translateY 负责将项定位到虚拟列表的正确位置,
// 它们拼接起来可以同时实现拖拽视觉效果和虚拟化定位。
const combinedTransform = dndStyle?.transform
? `${dndStyle.transform} ${virtualizerTransform}`
: virtualizerTransform
return (
<div
{...provided.draggableProps}
ref={setDragRefs}
className="draggable-item"
data-index={virtualItem.index}
style={{
...itemContainerStyle,
...dndStyle,
position: 'absolute',
top: 0,
left: 0,
width: '100%',
transform: combinedTransform
}}>
<div {...provided.dragHandleProps} className="draggable-content" style={itemStyle}>
{item && children(item, virtualItem.index)}
</div>
</div>
)
}}
</Draggable>
)
}
)
export default DraggableVirtualList

View File

@@ -1,117 +0,0 @@
// Original path: src/renderer/src/components/EditableNumber/index.tsx
import { InputNumber } from 'antd'
import type { FC } from 'react'
import { useEffect, useRef, useState } from 'react'
import styled from 'styled-components'
export interface EditableNumberProps {
value?: number | null
min?: number
max?: number
step?: number
precision?: number
placeholder?: string
disabled?: boolean
changeOnBlur?: boolean
onChange?: (value: number | null) => void
onBlur?: () => void
style?: React.CSSProperties
className?: string
size?: 'small' | 'middle' | 'large'
suffix?: string
prefix?: string
align?: 'start' | 'center' | 'end'
}
const EditableNumber: FC<EditableNumberProps> = ({
value,
min,
max,
step = 0.01,
precision,
placeholder,
disabled = false,
onChange,
onBlur,
changeOnBlur = false,
style,
className,
size = 'middle',
align = 'end'
}) => {
const [isEditing, setIsEditing] = useState(false)
const [inputValue, setInputValue] = useState(value)
const inputRef = useRef<HTMLInputElement>(null)
useEffect(() => {
setInputValue(value)
}, [value])
const handleFocus = () => {
if (disabled) return
setIsEditing(true)
}
const handleInputChange = (newValue: number | null) => {
onChange?.(newValue ?? null)
}
const handleBlur = () => {
setIsEditing(false)
onBlur?.()
}
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
handleBlur()
} else if (e.key === 'Escape') {
e.stopPropagation()
setInputValue(value)
setIsEditing(false)
}
}
return (
<Container>
<InputNumber
style={{ ...style, opacity: isEditing ? 1 : 0 }}
ref={inputRef}
value={inputValue}
min={min}
max={max}
step={step}
precision={precision}
size={size}
onChange={handleInputChange}
onBlur={handleBlur}
onFocus={handleFocus}
onKeyDown={handleKeyDown}
className={className}
controls={isEditing}
changeOnBlur={changeOnBlur}
/>
<DisplayText style={style} className={className} $align={align} $isEditing={isEditing}>
{value ?? placeholder}
</DisplayText>
</Container>
)
}
const Container = styled.div`
display: inline-block;
position: relative;
`
const DisplayText = styled.div<{
$align: 'start' | 'center' | 'end'
$isEditing: boolean
}>`
position: absolute;
inset: 0;
display: ${({ $isEditing }) => ($isEditing ? 'none' : 'flex')};
align-items: center;
justify-content: ${({ $align }) => $align};
pointer-events: none;
`
export default EditableNumber

View File

@@ -1,28 +0,0 @@
// Original: src/renderer/src/components/Ellipsis/index.tsx
import type { HTMLAttributes } from 'react'
import { cn } from '../../../utils'
type Props = {
maxLine?: number
className?: string
ref?: React.Ref<HTMLDivElement>
} & HTMLAttributes<HTMLDivElement>
const Ellipsis = (props: Props) => {
const { maxLine = 1, children, className, ref, ...rest } = props
const ellipsisClasses = cn(
'overflow-hidden text-ellipsis',
maxLine > 1 ? `line-clamp-${maxLine} break-words` : 'block whitespace-nowrap',
className
)
return (
<div ref={ref} className={ellipsisClasses} {...rest}>
{children}
</div>
)
}
export default Ellipsis

View File

@@ -1,50 +0,0 @@
// Original: src/renderer/src/components/ExpandableText.tsx
import { Button } from '@heroui/react'
import { memo, useCallback, useState } from 'react'
interface ExpandableTextProps {
text: string
style?: React.CSSProperties
className?: string
expandText?: string
collapseText?: string
lineClamp?: number
ref?: React.RefObject<HTMLDivElement>
}
const ExpandableText = ({
text,
style,
className = '',
expandText = 'Expand',
collapseText = 'Collapse',
lineClamp = 1,
ref
}: ExpandableTextProps) => {
const [isExpanded, setIsExpanded] = useState(false)
const toggleExpand = useCallback(() => {
setIsExpanded((prev) => !prev)
}, [])
return (
<div
ref={ref}
className={`flex ${isExpanded ? 'flex-col' : 'flex-row items-center'} gap-2 ${className}`}
style={style}>
<div
className={`overflow-hidden ${
isExpanded ? '' : lineClamp === 1 ? 'text-ellipsis whitespace-nowrap' : `line-clamp-${lineClamp}`
} ${isExpanded ? '' : 'flex-1'}`}>
{text}
</div>
<Button size="sm" variant="light" color="primary" onClick={toggleExpand} className="min-w-fit px-2">
{isExpanded ? collapseText : expandText}
</Button>
</div>
)
}
ExpandableText.displayName = 'ExpandableText'
export default memo(ExpandableText)

View File

@@ -1,63 +0,0 @@
import React from 'react'
import { cn } from '../../../utils'
export interface BoxProps extends React.ComponentProps<'div'> {}
export const Box = ({ children, className, ...props }: BoxProps & { children?: React.ReactNode }) => {
return (
<div className={cn('box-border', className)} {...props}>
{children}
</div>
)
}
export interface FlexProps extends BoxProps {}
export const Flex = ({ children, className, ...props }: FlexProps & { children?: React.ReactNode }) => {
return (
<Box className={cn('flex', className)} {...props}>
{children}
</Box>
)
}
export const RowFlex = ({ children, className, ...props }: FlexProps & { children?: React.ReactNode }) => {
return (
<Flex className={cn('flex-row', className)} {...props}>
{children}
</Flex>
)
}
export const SpaceBetweenRowFlex = ({ children, className, ...props }: FlexProps & { children?: React.ReactNode }) => {
return (
<RowFlex className={cn('justify-between', className)} {...props}>
{children}
</RowFlex>
)
}
export const ColFlex = ({ children, className, ...props }: FlexProps & { children?: React.ReactNode }) => {
return (
<Flex className={cn('flex-col', className)} {...props}>
{children}
</Flex>
)
}
export const Center = ({ children, className, ...props }: FlexProps & { children?: React.ReactNode }) => {
return (
<Flex className={cn('items-center justify-center', className)} {...props}>
{children}
</Flex>
)
}
export default {
Box,
Flex,
RowFlex,
SpaceBetweenRowFlex,
ColFlex,
Center
}

View File

@@ -1,181 +0,0 @@
// Original: src/renderer/src/components/HorizontalScrollContainer/index.tsx
import { ChevronRight } from 'lucide-react'
import { useEffect, useRef, useState } from 'react'
import styled from 'styled-components'
import Scrollbar from '../Scrollbar'
/**
* 水平滚动容器
* @param children 子元素
* @param dependencies 依赖项
* @param scrollDistance 滚动距离
* @param className 类名
* @param gap 间距
* @param expandable 是否可展开
*/
export interface HorizontalScrollContainerProps {
children: React.ReactNode
dependencies?: readonly unknown[]
scrollDistance?: number
className?: string
gap?: string
expandable?: boolean
}
const HorizontalScrollContainer: React.FC<HorizontalScrollContainerProps> = ({
children,
dependencies = [],
scrollDistance = 200,
className,
gap = '8px',
expandable = false
}) => {
const scrollRef = useRef<HTMLDivElement>(null)
const [canScroll, setCanScroll] = useState(false)
const [isExpanded, setIsExpanded] = useState(false)
const [isScrolledToEnd, setIsScrolledToEnd] = useState(false)
const handleScrollRight = (event: React.MouseEvent) => {
scrollRef.current?.scrollBy({ left: scrollDistance, behavior: 'smooth' })
event.stopPropagation()
}
const handleContainerClick = (e: React.MouseEvent) => {
if (expandable) {
// 确保不是点击了其他交互元素(如 tag 的关闭按钮)
const target = e.target as HTMLElement
if (!target.closest('[data-no-expand]')) {
setIsExpanded(!isExpanded)
}
}
}
const checkScrollability = () => {
const scrollElement = scrollRef.current
if (scrollElement) {
const parentElement = scrollElement.parentElement
const availableWidth = parentElement ? parentElement.clientWidth : scrollElement.clientWidth
// 确保容器不会超出可用宽度
const canScrollValue = scrollElement.scrollWidth > Math.min(availableWidth, scrollElement.clientWidth)
setCanScroll(canScrollValue)
// 检查是否滚动到最右侧
if (canScrollValue) {
const isAtEnd = Math.abs(scrollElement.scrollLeft + scrollElement.clientWidth - scrollElement.scrollWidth) <= 1
setIsScrolledToEnd(isAtEnd)
} else {
setIsScrolledToEnd(false)
}
}
}
useEffect(() => {
const scrollElement = scrollRef.current
if (!scrollElement) return
checkScrollability()
const handleScroll = () => {
checkScrollability()
}
const resizeObserver = new ResizeObserver(checkScrollability)
resizeObserver.observe(scrollElement)
scrollElement.addEventListener('scroll', handleScroll)
window.addEventListener('resize', checkScrollability)
return () => {
resizeObserver.disconnect()
scrollElement.removeEventListener('scroll', handleScroll)
window.removeEventListener('resize', checkScrollability)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, dependencies)
return (
<Container
className={className}
$expandable={expandable}
$disableHoverButton={isScrolledToEnd}
onClick={expandable ? handleContainerClick : undefined}>
<ScrollContent ref={scrollRef} $gap={gap} $isExpanded={isExpanded} $expandable={expandable}>
{children}
</ScrollContent>
{canScroll && !isExpanded && !isScrolledToEnd && (
<ScrollButton onClick={handleScrollRight} className="scroll-right-button">
<ChevronRight size={14} />
</ScrollButton>
)}
</Container>
)
}
const Container = styled.div<{ $expandable?: boolean; $disableHoverButton?: boolean }>`
display: flex;
align-items: center;
flex: 1 1 auto;
min-width: 0;
max-width: 100%;
position: relative;
cursor: ${(props) => (props.$expandable ? 'pointer' : 'default')};
${(props) =>
!props.$disableHoverButton &&
`
&:hover {
.scroll-right-button {
opacity: 1;
}
}
`}
`
const ScrollContent = styled(Scrollbar)<{
$gap: string
$isExpanded?: boolean
$expandable?: boolean
}>`
display: flex;
overflow-x: ${(props) => (props.$expandable && props.$isExpanded ? 'hidden' : 'auto')};
overflow-y: hidden;
white-space: ${(props) => (props.$expandable && props.$isExpanded ? 'normal' : 'nowrap')};
gap: ${(props) => props.$gap};
flex-wrap: ${(props) => (props.$expandable && props.$isExpanded ? 'wrap' : 'nowrap')};
&::-webkit-scrollbar {
display: none;
}
`
const ScrollButton = styled.div`
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
z-index: 1;
opacity: 0;
transition: opacity 0.2s ease-in-out;
cursor: pointer;
background: var(--color-background);
border-radius: 50%;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
box-shadow:
0 6px 16px 0 rgba(0, 0, 0, 0.08),
0 3px 6px -4px rgba(0, 0, 0, 0.12),
0 9px 28px 8px rgba(0, 0, 0, 0.05);
color: var(--color-text-2);
&:hover {
color: var(--color-text);
background: var(--color-list-item);
}
`
export default HorizontalScrollContainer

View File

@@ -1,19 +0,0 @@
// Original path: src/renderer/src/components/TooltipIcons/HelpTooltip.tsx
import { HelpCircle } from 'lucide-react'
import { Tooltip } from '../../primitives/tooltip'
import type { IconTooltipProps } from './types'
export const HelpTooltip = ({ iconProps, ...rest }: IconTooltipProps) => {
return (
<Tooltip {...rest}>
<HelpCircle
size={iconProps?.size ?? 14}
color={iconProps?.color ?? 'var(--color-text-2)'}
role="img"
aria-label="Help"
{...iconProps}
/>
</Tooltip>
)
}

View File

@@ -1,19 +0,0 @@
// Original: src/renderer/src/components/TooltipIcons/InfoTooltip.tsx
import { Info } from 'lucide-react'
import { Tooltip } from '../../primitives/tooltip'
import type { IconTooltipProps } from './types'
export const InfoTooltip = ({ iconProps, ...rest }: IconTooltipProps) => {
return (
<Tooltip {...rest}>
<Info
size={iconProps?.size ?? 14}
color={iconProps?.color ?? 'var(--color-text-2)'}
role="img"
aria-label="Information"
{...iconProps}
/>
</Tooltip>
)
}

View File

@@ -1,19 +0,0 @@
// Original path: src/renderer/src/components/TooltipIcons/WarnTooltip.tsx
import { AlertTriangle } from 'lucide-react'
import { Tooltip } from '../../primitives/tooltip'
import type { IconTooltipProps } from './types'
export const WarnTooltip = ({ iconProps, ...rest }: IconTooltipProps) => {
return (
<Tooltip {...rest}>
<AlertTriangle
size={iconProps?.size ?? 14}
color={iconProps?.color ?? 'var(--color-status-warning)'}
role="img"
aria-label="Warning"
{...iconProps}
/>
</Tooltip>
)
}

View File

@@ -1,4 +0,0 @@
export { HelpTooltip } from './HelpTooltip'
export { InfoTooltip } from './InfoTooltip'
export type { IconTooltipProps } from './types'
export { WarnTooltip } from './WarnTooltip'

View File

@@ -1,7 +0,0 @@
import type { LucideProps } from 'lucide-react'
import type { TooltipProps } from '../../primitives/tooltip'
export interface IconTooltipProps extends TooltipProps {
iconProps?: LucideProps
}

View File

@@ -1,23 +0,0 @@
// Original path: src/renderer/src/components/Preview/ImageToolButton.tsx
import { memo } from 'react'
import { Button } from '../../primitives/button'
import { Tooltip } from '../../primitives/tooltip'
interface ImageToolButtonProps {
tooltip: string
icon: React.ReactNode
onPress: () => void
}
const ImageToolButton = ({ tooltip, icon, onPress }: ImageToolButtonProps) => {
return (
<Tooltip content={tooltip} delay={500} closeDelay={0}>
<Button size="icon" className="rounded-full" onClick={onPress} aria-label={tooltip}>
{icon}
</Button>
</Tooltip>
)
}
export default memo(ImageToolButton)

View File

@@ -1,61 +0,0 @@
// Original path: src/renderer/src/components/ListItem/index.tsx
import { Tooltip } from '@heroui/react'
import type { ReactNode } from 'react'
import { cn } from '../../../utils'
interface ListItemProps {
active?: boolean
icon?: ReactNode
title: ReactNode
subtitle?: string
titleStyle?: React.CSSProperties
onClick?: () => void
rightContent?: ReactNode
style?: React.CSSProperties
className?: string
ref?: React.Ref<HTMLDivElement>
}
const ListItem = ({
active,
icon,
title,
subtitle,
titleStyle,
onClick,
rightContent,
style,
className,
ref
}: ListItemProps) => {
return (
<div
ref={ref}
className={cn(
'px-3 py-1.5 rounded-md text-xs flex flex-col justify-between relative cursor-pointer border border-transparent',
'hover:bg-gray-50 dark:hover:bg-gray-800',
active && 'bg-gray-50 dark:bg-gray-800 border-gray-200 dark:border-gray-700',
className
)}
onClick={onClick}
style={style}>
<div className="flex items-center gap-0.5 overflow-hidden text-xs">
{icon && <span className="flex items-center justify-center mr-2">{icon}</span>}
<div className="flex-1 flex flex-col overflow-hidden">
<Tooltip content={title}>
<div className="truncate text-gray-900 dark:text-gray-100" style={titleStyle}>
{title}
</div>
</Tooltip>
{subtitle && (
<div className="text-[10px] text-gray-500 dark:text-gray-400 mt-0.5 line-clamp-1">{subtitle}</div>
)}
</div>
{rightContent && <div className="ml-auto">{rightContent}</div>}
</div>
</div>
)
}
export default ListItem

View File

@@ -1,23 +0,0 @@
// Original path: src/renderer/src/components/MaxContextCount.tsx
import { Infinity as InfinityIcon } from 'lucide-react'
import type { CSSProperties } from 'react'
const MAX_CONTEXT_COUNT = 100
type Props = {
maxContext: number
style?: CSSProperties
size?: number
className?: string
ref?: React.Ref<HTMLSpanElement>
}
export default function MaxContextCount({ maxContext, style, size = 14, className, ref }: Props) {
return maxContext === MAX_CONTEXT_COUNT ? (
<InfinityIcon size={size} style={style} className={className} aria-label="infinity" />
) : (
<span ref={ref} style={style} className={className}>
{maxContext.toString()}
</span>
)
}

View File

@@ -1,76 +0,0 @@
// Original: src/renderer/src/components/Scrollbar/index.tsx
import { throttle } from 'lodash'
import type { FC } from 'react'
import { useCallback, useEffect, useRef, useState } from 'react'
import styled from 'styled-components'
export interface ScrollbarProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onScroll'> {
ref?: React.Ref<HTMLDivElement | null>
onScroll?: () => void // Custom onScroll prop for useScrollPosition's handleScroll
}
const Scrollbar: FC<ScrollbarProps> = ({ ref: passedRef, children, onScroll: externalOnScroll, ...htmlProps }) => {
const [isScrolling, setIsScrolling] = useState(false)
const timeoutRef = useRef<NodeJS.Timeout | null>(null)
const clearScrollingTimeout = useCallback(() => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current)
timeoutRef.current = null
}
}, [])
const handleScroll = useCallback(() => {
setIsScrolling(true)
clearScrollingTimeout()
timeoutRef.current = setTimeout(() => {
setIsScrolling(false)
timeoutRef.current = null
}, 1500)
}, [clearScrollingTimeout])
// eslint-disable-next-line react-hooks/exhaustive-deps
const throttledInternalScrollHandler = useCallback(throttle(handleScroll, 100, { leading: true, trailing: true }), [
handleScroll
])
// Combined scroll handler
const combinedOnScroll = useCallback(() => {
throttledInternalScrollHandler()
if (externalOnScroll) {
externalOnScroll()
}
}, [throttledInternalScrollHandler, externalOnScroll])
useEffect(() => {
return () => {
clearScrollingTimeout()
throttledInternalScrollHandler.cancel()
}
}, [throttledInternalScrollHandler, clearScrollingTimeout])
return (
<ScrollBarContainer
{...htmlProps} // Pass other HTML attributes
$isScrolling={isScrolling}
onScroll={combinedOnScroll} // Use the combined handler
ref={passedRef}>
{children}
</ScrollBarContainer>
)
}
const ScrollBarContainer = styled.div<{ $isScrolling: boolean }>`
overflow-y: auto;
&::-webkit-scrollbar-thumb {
transition: background 2s ease;
background: ${(props) => (props.$isScrolling ? 'var(--color-scrollbar-thumb)' : 'transparent')};
&:hover {
background: var(--color-scrollbar-thumb-hover);
}
}
`
Scrollbar.displayName = 'Scrollbar'
export default Scrollbar

View File

@@ -1,116 +0,0 @@
import type { DraggableSyntheticListeners } from '@dnd-kit/core'
import type { Transform } from '@dnd-kit/utilities'
import { CSS } from '@dnd-kit/utilities'
import React, { useEffect } from 'react'
import styled from 'styled-components'
import { cn } from '../../../utils'
import type { RenderItemType } from './types'
interface ItemRendererProps<T> {
ref?: React.Ref<HTMLDivElement>
index?: number
item: T
renderItem: RenderItemType<T>
dragging?: boolean
dragOverlay?: boolean
ghost?: boolean
transform?: Transform | null
transition?: string | null
listeners?: DraggableSyntheticListeners
itemStyle?: React.CSSProperties
}
export function ItemRenderer<T>({
ref,
index,
item,
renderItem,
dragging,
dragOverlay,
ghost,
transform,
transition,
listeners,
itemStyle,
...props
}: ItemRendererProps<T>) {
useEffect(() => {
if (!dragOverlay) {
return
}
document.body.style.cursor = 'grabbing'
return () => {
document.body.style.cursor = ''
}
}, [dragOverlay])
const style = {
transition,
transform: CSS.Transform.toString(transform ?? null)
} as React.CSSProperties
return (
<ItemWrapper
ref={ref}
data-index={index}
className={cn({ dragOverlay: dragOverlay })}
style={{ ...style, ...itemStyle }}>
<DraggableItem
className={cn({ dragging: dragging, dragOverlay: dragOverlay, ghost: ghost })}
{...listeners}
{...props}>
{renderItem(item, { dragging: !!dragging })}
</DraggableItem>
</ItemWrapper>
)
}
const ItemWrapper = styled.div`
box-sizing: border-box;
transform-origin: 0 0;
touch-action: manipulation;
&.dragOverlay {
--scale: 1.02;
z-index: 999;
position: relative;
}
`
const DraggableItem = styled.div`
position: relative;
box-sizing: border-box;
cursor: pointer; /* default cursor for items */
touch-action: manipulation;
transform-origin: 50% 50%;
transform: scale(var(--scale, 1));
&.dragging:not(.dragOverlay) {
z-index: 0;
opacity: 0.25;
&:not(.ghost) {
opacity: 0;
}
}
&.dragOverlay {
cursor: inherit;
animation: pop 200ms cubic-bezier(0.18, 0.67, 0.6, 1.22);
transform: scale(var(--scale));
opacity: 1;
pointer-events: none; /* prevent pointer events on drag overlay */
}
@keyframes pop {
0% {
transform: scale(1);
}
100% {
transform: scale(var(--scale));
}
}
`

View File

@@ -1 +0,0 @@
export { default as Sortable } from './Sortable'

View File

@@ -1,38 +0,0 @@
import type { Variants } from 'motion/react'
export const lightbulbVariants: Variants = {
active: {
opacity: [1, 0.2, 1],
transition: {
duration: 1.2,
ease: 'easeInOut',
times: [0, 0.5, 1],
repeat: Infinity
}
},
idle: {
opacity: 1,
transition: {
duration: 0.3,
ease: 'easeInOut'
}
}
}
export const lightbulbSoftVariants: Variants = {
active: {
opacity: [1, 0.5, 1],
transition: {
duration: 2,
ease: 'easeInOut',
times: [0, 0.5, 1],
repeat: Infinity
}
},
idle: {
opacity: 1,
transition: {
duration: 0.3,
ease: 'easeInOut'
}
}
}

View File

@@ -1,128 +0,0 @@
// Original path: src/renderer/src/components/ThinkingEffect.tsx
import { isEqual } from 'lodash'
import { ChevronRight, Lightbulb } from 'lucide-react'
import { motion } from 'motion/react'
import React, { useEffect, useMemo, useState } from 'react'
import { cn } from '../../../utils'
import { lightbulbVariants } from './defaultVariants'
interface ThinkingEffectProps {
isThinking: boolean
thinkingTimeText: React.ReactNode
content: string
expanded: boolean
className?: string
ref?: React.Ref<HTMLDivElement>
}
const ThinkingEffect: React.FC<ThinkingEffectProps> = ({
isThinking,
thinkingTimeText,
content,
expanded,
className,
ref
}) => {
const [messages, setMessages] = useState<string[]>([])
useEffect(() => {
const allLines = (content || '').split('\n')
const newMessages = isThinking ? allLines.slice(0, -1) : allLines
const validMessages = newMessages.filter((line) => line.trim() !== '')
if (!isEqual(messages, validMessages)) {
setMessages(validMessages)
}
}, [content, isThinking, messages])
const showThinking = useMemo(() => {
return isThinking && !expanded
}, [expanded, isThinking])
const LINE_HEIGHT = 14
const containerHeight = useMemo(() => {
if (!showThinking || messages.length < 1) return 38
return Math.min(75, Math.max(messages.length + 1, 2) * LINE_HEIGHT + 25)
}, [showThinking, messages.length])
return (
<div
ref={ref}
style={{ height: containerHeight }}
className={cn(
'w-full rounded-xl overflow-hidden relative flex items-center border-0.5 border-gray-200 dark:border-gray-700 transition-all duration-150 pointer-events-none select-none',
expanded && 'rounded-b-none',
className
)}>
<div className="w-12 flex justify-center items-center h-full flex-shrink-0 relative pl-1.5 transition-all duration-150">
<motion.div
variants={lightbulbVariants}
animate={isThinking ? 'active' : 'idle'}
initial="idle"
className="flex justify-center items-center">
<Lightbulb
size={!showThinking || messages.length < 2 ? 20 : 30}
style={{ transition: 'width,height, 150ms' }}
/>
</motion.div>
</div>
<div className="flex-1 h-full py-1.5 overflow-hidden relative">
<div
className={cn(
'absolute inset-x-0 top-0 text-sm leading-3.5 font-medium py-2.5 z-50 transition-all duration-150',
(!showThinking || !messages.length) && 'pt-3'
)}>
{thinkingTimeText}
</div>
{showThinking && (
<div
className="w-full h-full relative"
style={{
mask: 'linear-gradient(to bottom, rgb(0 0 0 / 0%) 0%, rgb(0 0 0 / 0%) 35%, rgb(0 0 0 / 25%) 40%, rgb(0 0 0 / 100%) 90%, rgb(0 0 0 / 100%) 100%)'
}}>
<motion.div
className="w-full absolute top-full flex flex-col justify-end"
style={{
height: messages.length * LINE_HEIGHT
}}
initial={{
y: -2
}}
animate={{
y: -messages.length * LINE_HEIGHT - 2
}}
transition={{
duration: 0.15,
ease: 'linear'
}}>
{messages.map((message, index) => {
if (index < messages.length - 5) return null
return (
<div
key={index}
className="w-full leading-3.5 text-xs text-gray-600 dark:text-gray-300 whitespace-nowrap overflow-hidden text-ellipsis">
{message}
</div>
)
})}
</motion.div>
</div>
)}
</div>
<div
className={cn(
'w-10 flex justify-center items-center h-full flex-shrink-0 relative text-gray-400 dark:text-gray-500 transition-transform duration-150',
expanded && 'rotate-90'
)}>
<ChevronRight size={20} strokeWidth={1} />
</div>
</div>
)
}
export default ThinkingEffect

View File

@@ -1,71 +0,0 @@
// Original path: src/renderer/src/components/Icons/FileIcons.tsx
import type { CSSProperties, SVGProps } from 'react'
interface BaseFileIconProps extends SVGProps<SVGSVGElement> {
size?: string | number
text?: string
}
const textStyle: CSSProperties = {
fontStyle: 'italic',
fontSize: '7.70985px',
lineHeight: 0.8,
fontFamily: "'Times New Roman'",
textAlign: 'center',
writingMode: 'horizontal-tb',
direction: 'ltr',
textAnchor: 'middle',
fill: 'none',
stroke: '#000000',
strokeWidth: '0.289119',
strokeLinejoin: 'round',
strokeDasharray: 'none'
}
const tspanStyle: CSSProperties = {
fontStyle: 'normal',
fontVariant: 'normal',
fontWeight: 'normal',
fontStretch: 'condensed',
fontSize: '7.70985px',
lineHeight: 0.8,
fontFamily: 'Arial',
fill: '#000000',
fillOpacity: 1,
strokeWidth: '0.289119',
strokeDasharray: 'none'
}
const BaseFileIcon = ({ size = '1.1em', text = 'SVG', ...props }: BaseFileIconProps) => (
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
version="1.1"
id="svg4"
xmlns="http://www.w3.org/2000/svg"
{...props}>
<defs id="defs4" />
<path d="m 14,2 v 4 a 2,2 0 0 0 2,2 h 4" id="path3" />
<path d="M 15,2 H 6 A 2,2 0 0 0 4,4 v 16 a 2,2 0 0 0 2,2 h 12 a 2,2 0 0 0 2,-2 V 7 Z" id="path4" />
<text
xmlSpace="preserve"
style={textStyle}
x="12.478625"
y="17.170216"
id="text4"
transform="scale(0.96196394,1.03954)">
<tspan id="tspan4" x="12.478625" y="17.170216" style={tspanStyle}>
{text}
</tspan>
</text>
</svg>
)
export const FileSvgIcon = (props: Omit<BaseFileIconProps, 'text'>) => <BaseFileIcon text="SVG" {...props} />
export const FilePngIcon = (props: Omit<BaseFileIconProps, 'text'>) => <BaseFileIcon text="PNG" {...props} />

View File

@@ -1,44 +0,0 @@
import type { LucideIcon } from 'lucide-react'
import {
AlignLeft,
Copy,
Eye,
Pencil,
RefreshCw,
RotateCcw,
ScanLine,
Search,
Trash,
WrapText,
Wrench
} from 'lucide-react'
import React from 'react'
// 创建一个 Icon 工厂函数
export function createIcon(IconComponent: LucideIcon, defaultSize: string | number = '1rem') {
const Icon = ({
ref,
...props
}: React.ComponentProps<typeof IconComponent> & { ref?: React.RefObject<SVGSVGElement | null> }) => (
<IconComponent ref={ref} size={defaultSize} {...props} />
)
Icon.displayName = `Icon(${IconComponent.displayName || IconComponent.name})`
return Icon
}
// 预定义的常用图标(向后兼容,只导入需要的图标)
export const CopyIcon = createIcon(Copy)
export const DeleteIcon = createIcon(Trash)
export const EditIcon = createIcon(Pencil)
export const RefreshIcon = createIcon(RefreshCw)
export const ResetIcon = createIcon(RotateCcw)
export const ToolIcon = createIcon(Wrench)
export const VisionIcon = createIcon(Eye)
export const WebSearchIcon = createIcon(Search)
export const WrapIcon = createIcon(WrapText)
export const UnWrapIcon = createIcon(AlignLeft)
export const OcrIcon = createIcon(ScanLine)
// 导出 createIcon 以便用户自行创建图标组件
export type { LucideIcon }
export type { LucideProps } from 'lucide-react'

View File

@@ -1,29 +0,0 @@
// Original path: src/renderer/src/components/Icons/SvgSpinners180Ring.tsx
import type { SVGProps } from 'react'
import { cn } from '../../../utils'
interface SvgSpinners180RingProps extends SVGProps<SVGSVGElement> {
size?: number | string
}
export function SvgSpinners180Ring(props: SvgSpinners180RingProps) {
const { size = '1em', className, ...svgProps } = props
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 24 24"
{...svgProps}
className={cn('animate-spin', className)}>
{/* Icon from SVG Spinners by Utkarsh Verma - https://github.com/n3r4zzurr0/svg-spinners/blob/main/LICENSE */}
<path
fill="currentColor"
d="M12,4a8,8,0,0,1,7.89,6.7A1.53,1.53,0,0,0,21.38,12h0a1.5,1.5,0,0,0,1.48-1.75,11,11,0,0,0-21.72,0A1.5,1.5,0,0,0,2.62,12h0a1.53,1.53,0,0,0,1.49-1.3A8,8,0,0,1,12,4Z"></path>
</svg>
)
}
export default SvgSpinners180Ring

View File

@@ -1,24 +0,0 @@
// Original: src/renderer/src/components/Icons/ToolsCallingIcon.tsx
import { Tooltip, type TooltipProps } from '@heroui/react'
import { Wrench } from 'lucide-react'
import React from 'react'
import { cn } from '../../../utils'
interface ToolsCallingIconProps extends React.HTMLAttributes<HTMLDivElement> {
className?: string
iconClassName?: string
TooltipProps?: TooltipProps
}
const ToolsCallingIcon = ({ className, iconClassName, TooltipProps, ...props }: ToolsCallingIconProps) => {
return (
<div className={cn('flex justify-center items-center', className)} {...props}>
<Tooltip {...TooltipProps}>
<Wrench className={cn('w-4 h-4 mr-1.5 text-[#00b96b]', iconClassName)} />
</Tooltip>
</div>
)
}
export default ToolsCallingIcon

View File

@@ -1,89 +0,0 @@
// Primitive Components
export { Avatar, AvatarGroup, type AvatarProps, EmojiAvatar } from './primitives/Avatar'
export { default as CopyButton } from './primitives/copyButton'
export { default as CustomTag } from './primitives/customTag'
export { default as DividerWithText } from './primitives/dividerWithText'
export { default as EmojiIcon } from './primitives/emojiIcon'
export type { CustomFallbackProps, ErrorBoundaryCustomizedProps } from './primitives/ErrorBoundary'
export { ErrorBoundary } from './primitives/ErrorBoundary'
export { default as IndicatorLight } from './primitives/indicatorLight'
export { default as Spinner } from './primitives/spinner'
export { DescriptionSwitch, Switch } from './primitives/switch'
export { getToastUtilities, type ToastUtilities } from './primitives/toast'
export { Tooltip, type TooltipProps } from './primitives/tooltip'
// Composite Components
export { default as Ellipsis } from './composites/Ellipsis'
export { default as ExpandableText } from './composites/ExpandableText'
export { Box, Center, ColFlex, Flex, RowFlex, SpaceBetweenRowFlex } from './composites/Flex'
export { default as HorizontalScrollContainer } from './composites/HorizontalScrollContainer'
export { default as ListItem } from './composites/ListItem'
export { default as MaxContextCount } from './composites/MaxContextCount'
export { default as Scrollbar } from './composites/Scrollbar'
export { default as ThinkingEffect } from './composites/ThinkingEffect'
// Icon Components
export { FilePngIcon, FileSvgIcon } from './icons/FileIcons'
export type { LucideIcon, LucideProps } from './icons/Icon'
export {
CopyIcon,
createIcon,
DeleteIcon,
EditIcon,
OcrIcon,
RefreshIcon,
ResetIcon,
ToolIcon,
UnWrapIcon,
VisionIcon,
WebSearchIcon,
WrapIcon
} from './icons/Icon'
export { default as SvgSpinners180Ring } from './icons/SvgSpinners180Ring'
export { default as ToolsCallingIcon } from './icons/ToolsCallingIcon'
/* Selector Components */
export { default as Selector } from './primitives/Selector'
export { default as SearchableSelector } from './primitives/Selector/SearchableSelector'
export type {
MultipleSearchableSelectorProps,
MultipleSelectorProps,
SearchableSelectorItem,
SearchableSelectorProps,
SelectorItem,
SelectorProps,
SingleSearchableSelectorProps,
SingleSelectorProps
} from './primitives/Selector/types'
/* Additional Composite Components */
// CodeEditor
export {
default as CodeEditor,
type CodeEditorHandles,
type CodeEditorProps,
type CodeMirrorTheme,
getCmThemeByName,
getCmThemeNames
} from './composites/CodeEditor'
// CollapsibleSearchBar
export { default as CollapsibleSearchBar } from './composites/CollapsibleSearchBar'
// DraggableList
export { DraggableList, useDraggableReorder } from './composites/DraggableList'
// EditableNumber
export type { EditableNumberProps } from './composites/EditableNumber'
export { default as EditableNumber } from './composites/EditableNumber'
// Tooltip variants
export { HelpTooltip, type IconTooltipProps, InfoTooltip, WarnTooltip } from './composites/IconTooltips'
// ImageToolButton
export { default as ImageToolButton } from './composites/ImageToolButton'
// Sortable
export { Sortable } from './composites/Sortable'
/* Shadcn Primitive Components */
export * from './primitives/button'
export * from './primitives/command'
export * from './primitives/dialog'
export * from './primitives/popover'
export * from './primitives/radioGroup'
export * from './primitives/shadcn-io/dropzone'

View File

@@ -1,37 +0,0 @@
import React, { memo } from 'react'
import { cn } from '../../../utils'
interface EmojiAvatarProps {
children: string
size?: number
fontSize?: number
onClick?: React.MouseEventHandler<HTMLDivElement>
className?: string
style?: React.CSSProperties
}
const EmojiAvatar = ({ children, size = 31, fontSize, onClick, className, style }: EmojiAvatarProps) => (
<div
onClick={onClick}
className={cn(
'flex items-center justify-center',
'bg-background-soft border-border',
'rounded-[20%] cursor-pointer',
'transition-opacity hover:opacity-80',
'border-[0.5px]',
className
)}
style={{
width: size,
height: size,
fontSize: fontSize ?? size * 0.5,
...style
}}>
{children}
</div>
)
EmojiAvatar.displayName = 'EmojiAvatar'
export default memo(EmojiAvatar)

View File

@@ -1,27 +0,0 @@
import type { AvatarProps as HeroUIAvatarProps } from '@heroui/react'
import { Avatar as HeroUIAvatar, AvatarGroup as HeroUIAvatarGroup } from '@heroui/react'
import { cn } from '../../../utils'
import EmojiAvatar from './EmojiAvatar'
export interface AvatarProps extends Omit<HeroUIAvatarProps, 'size'> {
size?: 'xs' | 'sm' | 'md' | 'lg'
}
const Avatar = (props: AvatarProps) => {
const { size, className = '', ...rest } = props
const isExtraSmall = size === 'xs'
const resolvedSize = isExtraSmall ? undefined : size
const mergedClassName = cn(isExtraSmall && 'w-6 h-6 text-tiny', 'shadow-lg', className)
return <HeroUIAvatar size={resolvedSize} className={mergedClassName} {...rest} />
}
Avatar.displayName = 'Avatar'
const AvatarGroup = HeroUIAvatarGroup
AvatarGroup.displayName = 'AvatarGroup'
export { Avatar, AvatarGroup, EmojiAvatar }

View File

@@ -1,94 +0,0 @@
// Original path: src/renderer/src/components/ErrorBoundary.tsx
import { AlertTriangle } from 'lucide-react'
import type { ComponentType, ReactNode } from 'react'
import type { FallbackProps } from 'react-error-boundary'
import { ErrorBoundary } from 'react-error-boundary'
import { Button } from '../button'
import { formatErrorMessage } from './utils'
interface CustomFallbackProps extends FallbackProps {
onDebugClick?: () => void | Promise<void>
onReloadClick?: () => void | Promise<void>
debugButtonText?: string
reloadButtonText?: string
errorMessage?: string
}
const DefaultFallback: ComponentType<CustomFallbackProps> = (props: CustomFallbackProps): ReactNode => {
const {
error,
onDebugClick,
onReloadClick,
debugButtonText = 'Open DevTools',
reloadButtonText = 'Reload',
errorMessage = 'An error occurred'
} = props
return (
<div className="flex justify-center items-center w-full p-2">
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4 w-full">
<div className="flex items-start gap-3">
<AlertTriangle className="text-red-500 dark:text-red-400 flex-shrink-0 mt-0.5" size={20} />
<div className="flex-1">
<h3 className="text-red-800 dark:text-red-200 font-medium text-sm mb-1">{errorMessage}</h3>
<p className="text-red-700 dark:text-red-300 text-sm mb-3">{formatErrorMessage(error)}</p>
<div className="flex gap-2">
{onDebugClick && (
<Button size="sm" variant="destructive" onClick={onDebugClick}>
{debugButtonText}
</Button>
)}
{onReloadClick && (
<Button size="sm" variant="destructive" onClick={onReloadClick}>
{reloadButtonText}
</Button>
)}
</div>
</div>
</div>
</div>
</div>
)
}
interface ErrorBoundaryCustomizedProps {
children: ReactNode
fallbackComponent?: ComponentType<CustomFallbackProps>
onDebugClick?: () => void | Promise<void>
onReloadClick?: () => void | Promise<void>
debugButtonText?: string
reloadButtonText?: string
errorMessage?: string
}
const ErrorBoundaryCustomized = ({
children,
fallbackComponent,
onDebugClick,
onReloadClick,
debugButtonText,
reloadButtonText,
errorMessage
}: ErrorBoundaryCustomizedProps) => {
const FallbackComponent = fallbackComponent ?? DefaultFallback
return (
<ErrorBoundary
FallbackComponent={(props: FallbackProps) => (
<FallbackComponent
{...props}
onDebugClick={onDebugClick}
onReloadClick={onReloadClick}
debugButtonText={debugButtonText}
reloadButtonText={reloadButtonText}
errorMessage={errorMessage}
/>
)}>
{children}
</ErrorBoundary>
)
}
export { ErrorBoundaryCustomized as ErrorBoundary }
export type { CustomFallbackProps, ErrorBoundaryCustomizedProps }

View File

@@ -1,8 +0,0 @@
// Utility functions for ErrorBoundary component
export function formatErrorMessage(error: Error): string {
if (error.message) {
return error.message
}
return error.toString()
}

View File

@@ -1,60 +0,0 @@
import { Autocomplete, AutocompleteItem } from '@heroui/react'
import type { Key } from '@react-types/shared'
import { useMemo } from 'react'
import type { SearchableSelectorItem, SearchableSelectorProps } from './types'
const SearchableSelector = <T extends SearchableSelectorItem>(props: SearchableSelectorProps<T>) => {
const { items, onSelectionChange, selectedKeys, selectionMode = 'single', children, ...rest } = props
// 转换 selectedKeys: V | V[] → Key | undefined (Autocomplete 只支持单选)
const autocompleteSelectedKey = useMemo(() => {
if (selectedKeys === undefined) return undefined
if (selectionMode === 'multiple') {
// Autocomplete 不支持多选,取第一个
const keys = selectedKeys as T['value'][]
return keys.length > 0 ? String(keys[0]) : undefined
} else {
return String(selectedKeys)
}
}, [selectedKeys, selectionMode])
// 处理选择变化
const handleSelectionChange = (key: Key | null) => {
if (!onSelectionChange || key === null) return
const strKey = String(key)
// 尝试转换回数字类型
const num = Number(strKey)
const value = !isNaN(num) && items.some((item) => item.value === num) ? (num as T['value']) : (strKey as T['value'])
if (selectionMode === 'multiple') {
// 多选模式: 返回数组 (Autocomplete 只支持单选,这里简化处理)
;(onSelectionChange as (keys: T['value'][]) => void)([value])
} else {
// 单选模式: 返回单个值
;(onSelectionChange as (key: T['value']) => void)(value)
}
}
// 默认渲染函数
const defaultRenderItem = (item: T) => (
<AutocompleteItem key={String(item.value)} textValue={item.label ? String(item.label) : String(item.value)}>
{item.label ?? item.value}
</AutocompleteItem>
)
return (
<Autocomplete
{...rest}
items={items}
selectedKey={autocompleteSelectedKey}
onSelectionChange={handleSelectionChange}
allowsCustomValue={false}>
{children ?? defaultRenderItem}
</Autocomplete>
)
}
export default SearchableSelector

View File

@@ -1,75 +0,0 @@
import type { Selection } from '@heroui/react'
import { Select, SelectItem } from '@heroui/react'
import type { Key } from '@react-types/shared'
import { useMemo } from 'react'
import type { SelectorItem, SelectorProps } from './types'
const Selector = <T extends SelectorItem>(props: SelectorProps<T>) => {
const { items, onSelectionChange, selectedKeys, selectionMode = 'single', children, ...rest } = props
// 转换 selectedKeys: V | V[] | undefined → Set<Key> | undefined
const heroUISelectedKeys = useMemo(() => {
if (selectedKeys === undefined) return undefined
if (selectionMode === 'multiple') {
// 多选模式: V[] → Set<Key>
return new Set((selectedKeys as T['value'][]).map((key) => String(key) as Key))
} else {
// 单选模式: V → Set<Key>
return new Set([String(selectedKeys) as Key])
}
}, [selectedKeys, selectionMode])
// 处理选择变化,转换 Selection → V | V[]
const handleSelectionChange = (keys: Selection) => {
if (!onSelectionChange) return
if (keys === 'all') {
// 如果是全选,返回所有非禁用项的值
const allValues = items.filter((item) => !item.disabled).map((item) => item.value)
if (selectionMode === 'multiple') {
;(onSelectionChange as (keys: T['value'][]) => void)(allValues)
}
return
}
// 转换 Set<Key> 为原始类型
const keysArray = Array.from(keys).map((key) => {
const strKey = String(key)
// 尝试转换回数字类型(如果原始值是数字)
const num = Number(strKey)
return !isNaN(num) && items.some((item) => item.value === num) ? (num as T['value']) : (strKey as T['value'])
})
if (selectionMode === 'multiple') {
// 多选模式: 返回数组
;(onSelectionChange as (keys: T['value'][]) => void)(keysArray)
} else {
// 单选模式: 返回单个值
if (keysArray.length > 0) {
;(onSelectionChange as (key: T['value']) => void)(keysArray[0])
}
}
}
// 默认渲染函数
const defaultRenderItem = (item: T) => (
<SelectItem key={String(item.value)} textValue={item.label ? String(item.label) : String(item.value)}>
{item.label ?? item.value}
</SelectItem>
)
return (
<Select
{...rest}
items={items}
selectionMode={selectionMode}
selectedKeys={heroUISelectedKeys as 'all' | Iterable<Key> | undefined}
onSelectionChange={handleSelectionChange}>
{children ?? defaultRenderItem}
</Select>
)
}
export default Selector

View File

@@ -1,13 +0,0 @@
// 统一导出 Selector 相关组件和类型
export { default as SearchableSelector } from './SearchableSelector'
export { default } from './Selector'
export type {
MultipleSearchableSelectorProps,
MultipleSelectorProps,
SearchableSelectorItem,
SearchableSelectorProps,
SelectorItem,
SelectorProps,
SingleSearchableSelectorProps,
SingleSelectorProps
} from './types'

View File

@@ -1,79 +0,0 @@
import type { AutocompleteProps, SelectProps } from '@heroui/react'
import type { ReactElement, ReactNode } from 'react'
interface SelectorItem<V = string | number> {
label?: string | ReactNode
value: V
disabled?: boolean
[key: string]: any
}
// 自定义渲染函数类型
type SelectorRenderItem<T> = (item: T) => ReactElement
// 单选模式的 Props
interface SingleSelectorProps<T extends SelectorItem = SelectorItem>
extends Omit<SelectProps<T>, 'children' | 'onSelectionChange' | 'selectedKeys' | 'selectionMode'> {
items: T[]
selectionMode?: 'single'
selectedKeys?: T['value']
onSelectionChange?: (key: T['value']) => void
children?: SelectorRenderItem<T>
}
// 多选模式的 Props
interface MultipleSelectorProps<T extends SelectorItem = SelectorItem>
extends Omit<SelectProps<T>, 'children' | 'onSelectionChange' | 'selectedKeys' | 'selectionMode'> {
items: T[]
selectionMode: 'multiple'
selectedKeys?: T['value'][]
onSelectionChange?: (keys: T['value'][]) => void
children?: SelectorRenderItem<T>
}
type SelectorProps<T extends SelectorItem = SelectorItem> = SingleSelectorProps<T> | MultipleSelectorProps<T>
interface SearchableSelectorItem<V = string | number> {
label?: string | ReactNode
value: V
disabled?: boolean
[key: string]: any
}
// 自定义渲染函数类型
type SearchableRenderItem<T> = (item: T) => ReactElement
// 单选模式的 Props
interface SingleSearchableSelectorProps<T extends SearchableSelectorItem = SearchableSelectorItem>
extends Omit<AutocompleteProps<T>, 'children' | 'onSelectionChange' | 'selectedKey' | 'selectionMode'> {
items: T[]
selectionMode?: 'single'
selectedKeys?: T['value']
onSelectionChange?: (key: T['value']) => void
children?: SearchableRenderItem<T>
}
// 多选模式的 Props
interface MultipleSearchableSelectorProps<T extends SearchableSelectorItem = SearchableSelectorItem>
extends Omit<AutocompleteProps<T>, 'children' | 'onSelectionChange' | 'selectedKey' | 'selectionMode'> {
items: T[]
selectionMode: 'multiple'
selectedKeys?: T['value'][]
onSelectionChange?: (keys: T['value'][]) => void
children?: SearchableRenderItem<T>
}
type SearchableSelectorProps<T extends SearchableSelectorItem = SearchableSelectorItem> =
| SingleSearchableSelectorProps<T>
| MultipleSearchableSelectorProps<T>
export type {
MultipleSearchableSelectorProps,
MultipleSelectorProps,
SearchableSelectorItem,
SearchableSelectorProps,
SelectorItem,
SelectorProps,
SingleSearchableSelectorProps,
SingleSelectorProps
}

View File

@@ -1,51 +0,0 @@
import { cn } from '@cherrystudio/ui/utils/index'
import { Slot } from '@radix-ui/react-slot'
import { cva, type VariantProps } from 'class-variance-authority'
import * as React from 'react'
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
destructive:
'bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
outline:
'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
link: 'text-primary underline-offset-4 hover:underline'
},
size: {
default: 'h-9 px-4 py-2 has-[>svg]:px-3',
sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',
lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
icon: 'size-9',
'icon-sm': 'size-8',
'icon-lg': 'size-10'
}
},
defaultVariants: {
variant: 'default',
size: 'default'
}
}
)
function Button({
className,
variant,
size,
asChild = false,
...props
}: React.ComponentProps<'button'> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot : 'button'
return <Comp data-slot="button" className={cn(buttonVariants({ variant, size, className }))} {...props} />
}
export { Button, buttonVariants }

View File

@@ -1,140 +0,0 @@
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle
} from '@cherrystudio/ui/components/primitives/dialog'
import { cn } from '@cherrystudio/ui/utils'
import { Command as CommandPrimitive } from 'cmdk'
import { SearchIcon } from 'lucide-react'
import * as React from 'react'
function Command({ className, ...props }: React.ComponentProps<typeof CommandPrimitive>) {
return (
<CommandPrimitive
data-slot="command"
className={cn(
'bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md',
className
)}
{...props}
/>
)
}
function CommandDialog({
title = 'Command Palette',
description = 'Search for a command to run...',
children,
className,
showCloseButton = true,
...props
}: React.ComponentProps<typeof Dialog> & {
title?: string
description?: string
className?: string
showCloseButton?: boolean
}) {
return (
<Dialog {...props}>
<DialogHeader className="sr-only">
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<DialogContent className={cn('overflow-hidden p-0', className)} showCloseButton={showCloseButton}>
<Command className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
{children}
</Command>
</DialogContent>
</Dialog>
)
}
function CommandInput({ className, ...props }: React.ComponentProps<typeof CommandPrimitive.Input>) {
return (
<div data-slot="command-input-wrapper" className="flex h-9 items-center gap-2 border-b px-3">
<SearchIcon className="size-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
data-slot="command-input"
className={cn(
'placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50',
className
)}
{...props}
/>
</div>
)
}
function CommandList({ className, ...props }: React.ComponentProps<typeof CommandPrimitive.List>) {
return (
<CommandPrimitive.List
data-slot="command-list"
className={cn('max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto', className)}
{...props}
/>
)
}
function CommandEmpty({ ...props }: React.ComponentProps<typeof CommandPrimitive.Empty>) {
return <CommandPrimitive.Empty data-slot="command-empty" className="py-6 text-center text-sm" {...props} />
}
function CommandGroup({ className, ...props }: React.ComponentProps<typeof CommandPrimitive.Group>) {
return (
<CommandPrimitive.Group
data-slot="command-group"
className={cn(
'text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium',
className
)}
{...props}
/>
)
}
function CommandSeparator({ className, ...props }: React.ComponentProps<typeof CommandPrimitive.Separator>) {
return (
<CommandPrimitive.Separator
data-slot="command-separator"
className={cn('bg-border -mx-1 h-px', className)}
{...props}
/>
)
}
function CommandItem({ className, ...props }: React.ComponentProps<typeof CommandPrimitive.Item>) {
return (
<CommandPrimitive.Item
data-slot="command-item"
className={cn(
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function CommandShortcut({ className, ...props }: React.ComponentProps<'span'>) {
return (
<span
data-slot="command-shortcut"
className={cn('text-muted-foreground ml-auto text-xs tracking-widest', className)}
{...props}
/>
)
}
export {
Command,
CommandDialog,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandSeparator,
CommandShortcut
}

View File

@@ -1,32 +0,0 @@
// Original path: src/renderer/src/components/CopyButton.tsx
import { Copy } from 'lucide-react'
import type { FC } from 'react'
import { Tooltip } from './tooltip'
interface CopyButtonProps {
tooltip?: string
label?: string
size?: number
className?: string
[key: string]: any
}
const CopyButton: FC<CopyButtonProps> = ({ tooltip, label, size = 14, className = '', ...props }) => {
const button = (
<div
className={`flex flex-row items-center gap-1 cursor-pointer text-gray-600 dark:text-gray-400 transition-colors duration-200 hover:text-blue-600 dark:hover:text-blue-400 ${className}`}
{...props}>
<Copy size={size} className="transition-colors duration-200" />
{label && <span style={{ fontSize: `${size}px` }}>{label}</span>}
</div>
)
if (tooltip) {
return <Tooltip content={tooltip}>{button}</Tooltip>
}
return button
}
export default CopyButton

View File

@@ -1,88 +0,0 @@
// Original path: src/renderer/src/components/Tags/CustomTag.tsx
import { X } from 'lucide-react'
import type { CSSProperties, FC, MouseEventHandler } from 'react'
import { memo, useMemo } from 'react'
import { Tooltip } from './tooltip'
export interface CustomTagProps {
icon?: React.ReactNode
children?: React.ReactNode | string
color: string
size?: number
style?: CSSProperties
tooltip?: string
closable?: boolean
onClose?: () => void
onClick?: MouseEventHandler<HTMLDivElement>
disabled?: boolean
inactive?: boolean
className?: string
}
const CustomTag: FC<CustomTagProps> = ({
children,
icon,
color,
size = 12,
style,
tooltip,
closable = false,
onClose,
onClick,
disabled,
inactive,
className = ''
}) => {
const actualColor = inactive ? '#aaaaaa' : color
const tagContent = useMemo(
() => (
<div
className={`inline-flex items-center gap-1 rounded-full whitespace-nowrap relative transition-opacity duration-200 ${
!disabled && onClick ? 'cursor-pointer hover:opacity-80' : disabled ? 'cursor-not-allowed' : 'cursor-auto'
} ${className}`}
style={{
padding: `${size / 3}px ${closable ? size * 1.8 : size * 0.8}px ${size / 3}px ${size * 0.8}px`,
color: actualColor,
backgroundColor: actualColor + '20',
fontSize: `${size}px`,
lineHeight: 1,
...style
}}
onClick={disabled ? undefined : onClick}>
{icon && <span style={{ fontSize: `${size}px`, color: actualColor }}>{icon}</span>}
{children}
{closable && (
<div
className="absolute flex items-center justify-center cursor-pointer rounded-full transition-all duration-200 hover:bg-[#da8a8a] hover:text-white"
style={{
right: `${size * 0.2}px`,
top: `${size * 0.2}px`,
bottom: `${size * 0.2}px`,
fontSize: `${size * 0.8}px`,
color: actualColor,
aspectRatio: 1
}}
onClick={(e) => {
e.stopPropagation()
onClose?.()
}}>
<X size={size * 0.8} />
</div>
)}
</div>
),
[actualColor, children, closable, disabled, icon, onClick, onClose, size, style, className]
)
return tooltip ? (
<Tooltip content={tooltip} delay={300}>
{tagContent}
</Tooltip>
) : (
tagContent
)
}
export default memo(CustomTag)

View File

@@ -1,118 +0,0 @@
import { cn } from '@cherrystudio/ui/utils/index'
import * as DialogPrimitive from '@radix-ui/react-dialog'
import { XIcon } from 'lucide-react'
import * as React from 'react'
function Dialog({ ...props }: React.ComponentProps<typeof DialogPrimitive.Root>) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />
}
function DialogTrigger({ ...props }: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
}
function DialogPortal({ ...props }: React.ComponentProps<typeof DialogPrimitive.Portal>) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
}
function DialogClose({ ...props }: React.ComponentProps<typeof DialogPrimitive.Close>) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
}
function DialogOverlay({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
return (
<DialogPrimitive.Overlay
data-slot="dialog-overlay"
className={cn(
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50',
className
)}
{...props}
/>
)
}
function DialogContent({
className,
children,
showCloseButton = true,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
showCloseButton?: boolean
}) {
return (
<DialogPortal data-slot="dialog-portal">
<DialogOverlay />
<DialogPrimitive.Content
data-slot="dialog-content"
className={cn(
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg',
className
)}
{...props}>
{children}
{showCloseButton && (
<DialogPrimitive.Close
data-slot="dialog-close"
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4">
<XIcon />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Content>
</DialogPortal>
)
}
function DialogHeader({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="dialog-header"
className={cn('flex flex-col gap-2 text-center sm:text-left', className)}
{...props}
/>
)
}
function DialogFooter({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="dialog-footer"
className={cn('flex flex-col-reverse gap-2 sm:flex-row sm:justify-end', className)}
{...props}
/>
)
}
function DialogTitle({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Title>) {
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn('text-lg leading-none font-semibold', className)}
{...props}
/>
)
}
function DialogDescription({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Description>) {
return (
<DialogPrimitive.Description
data-slot="dialog-description"
className={cn('text-muted-foreground text-sm', className)}
{...props}
/>
)
}
export {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger
}

View File

@@ -1,20 +0,0 @@
// Original: src/renderer/src/components/DividerWithText.tsx
import type { CSSProperties } from 'react'
import React from 'react'
interface DividerWithTextProps {
text: string
style?: CSSProperties
className?: string
}
const DividerWithText: React.FC<DividerWithTextProps> = ({ text, style, className = '' }) => {
return (
<div className={`flex items-center my-0 ${className}`} style={style}>
<span className="text-xs text-gray-600 dark:text-gray-400 mr-2">{text}</span>
<div className="flex-1 h-px bg-gray-200 dark:bg-gray-700" />
</div>
)
}
export default DividerWithText

View File

@@ -1,34 +0,0 @@
// Original path: src/renderer/src/components/EmojiIcon.tsx
import type { FC } from 'react'
interface EmojiIconProps {
emoji: string
className?: string
size?: number
fontSize?: number
}
const EmojiIcon: FC<EmojiIconProps> = ({ emoji, className = '', size = 26, fontSize = 15 }) => {
return (
<div
className={`flex items-center justify-center flex-shrink-0 relative overflow-hidden mr-1 rounded-full ${className}`}
style={{
width: `${size}px`,
height: `${size}px`,
borderRadius: `${size / 2}px`,
fontSize: `${fontSize}px`
}}>
<div
className="absolute inset-0 flex items-center justify-center blur-sm opacity-40"
style={{
fontSize: '200%',
transform: 'scale(1.5)'
}}>
{emoji || '⭐️'}
</div>
{emoji}
</div>
)
}
export default EmojiIcon

View File

@@ -1,37 +0,0 @@
// Original: src/renderer/src/components/IndicatorLight.tsx
import React from 'react'
interface IndicatorLightProps {
color: string
size?: number
shadow?: boolean
style?: React.CSSProperties
animation?: boolean
className?: string
}
const IndicatorLight: React.FC<IndicatorLightProps> = ({
color,
size = 8,
shadow = true,
style,
animation = true,
className = ''
}) => {
const actualColor = color === 'green' ? '#22c55e' : color
return (
<div
className={`rounded-full ${animation ? 'animate-pulse' : ''} ${className}`}
style={{
width: `${size}px`,
height: `${size}px`,
backgroundColor: actualColor,
boxShadow: shadow ? `0 0 6px ${actualColor}` : 'none',
...style
}}
/>
)
}
export default IndicatorLight

View File

@@ -1,41 +0,0 @@
'use client'
import { cn } from '@cherrystudio/ui/utils'
import * as PopoverPrimitive from '@radix-ui/react-popover'
import * as React from 'react'
function Popover({ ...props }: React.ComponentProps<typeof PopoverPrimitive.Root>) {
return <PopoverPrimitive.Root data-slot="popover" {...props} />
}
function PopoverTrigger({ ...props }: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
}
function PopoverContent({
className,
align = 'center',
sideOffset = 4,
...props
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
return (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
data-slot="popover-content"
align={align}
sideOffset={sideOffset}
className={cn(
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden',
className
)}
{...props}
/>
</PopoverPrimitive.Portal>
)
}
function PopoverAnchor({ ...props }: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
}
export { Popover, PopoverAnchor, PopoverContent, PopoverTrigger }

View File

@@ -1,28 +0,0 @@
import { cn } from '@cherrystudio/ui/utils/index'
import * as RadioGroupPrimitive from '@radix-ui/react-radio-group'
import { CircleIcon } from 'lucide-react'
import * as React from 'react'
function RadioGroup({ className, ...props }: React.ComponentProps<typeof RadioGroupPrimitive.Root>) {
return <RadioGroupPrimitive.Root data-slot="radio-group" className={cn('grid gap-3', className)} {...props} />
}
function RadioGroupItem({ className, ...props }: React.ComponentProps<typeof RadioGroupPrimitive.Item>) {
return (
<RadioGroupPrimitive.Item
data-slot="radio-group-item"
className={cn(
'border-input text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50',
className
)}
{...props}>
<RadioGroupPrimitive.Indicator
data-slot="radio-group-indicator"
className="relative flex items-center justify-center">
<CircleIcon className="fill-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2" />
</RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item>
)
}
export { RadioGroup, RadioGroupItem }

View File

@@ -1,178 +0,0 @@
'use client'
import { Button } from '@cherrystudio/ui/components/primitives/button'
import { cn } from '@cherrystudio/ui/utils/index'
import { UploadIcon } from 'lucide-react'
import type { ReactNode } from 'react'
import { createContext, use } from 'react'
import type { DropEvent, DropzoneOptions, FileRejection } from 'react-dropzone'
import { useDropzone } from 'react-dropzone'
type DropzoneContextType = {
src?: File[]
accept?: DropzoneOptions['accept']
maxSize?: DropzoneOptions['maxSize']
minSize?: DropzoneOptions['minSize']
maxFiles?: DropzoneOptions['maxFiles']
}
const renderBytes = (bytes: number) => {
const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']
let size = bytes
let unitIndex = 0
while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024
unitIndex++
}
return `${size.toFixed(2)}${units[unitIndex]}`
}
const DropzoneContext = createContext<DropzoneContextType | undefined>(undefined)
export type DropzoneProps = Omit<DropzoneOptions, 'onDrop'> & {
src?: File[]
className?: string
onDrop?: (acceptedFiles: File[], fileRejections: FileRejection[], event: DropEvent) => void
children?: ReactNode
}
export const Dropzone = ({
accept,
maxFiles = 1,
maxSize,
minSize,
onDrop,
onError,
disabled,
src,
className,
children,
...props
}: DropzoneProps) => {
const { getRootProps, getInputProps, isDragActive } = useDropzone({
accept,
maxFiles,
maxSize,
minSize,
onError,
disabled,
onDrop: (acceptedFiles, fileRejections, event) => {
if (fileRejections.length > 0) {
const message = fileRejections.at(0)?.errors.at(0)?.message
onError?.(new Error(message))
return
}
onDrop?.(acceptedFiles, fileRejections, event)
},
...props
})
return (
<DropzoneContext key={JSON.stringify(src)} value={{ src, accept, maxSize, minSize, maxFiles }}>
<Button
className={cn(
'relative h-auto w-full flex-col overflow-hidden p-8',
isDragActive && 'outline-none ring-1 ring-ring',
className
)}
disabled={disabled}
type="button"
variant="outline"
{...getRootProps()}>
<input {...getInputProps()} disabled={disabled} />
{children}
</Button>
</DropzoneContext>
)
}
const useDropzoneContext = () => {
const context = use(DropzoneContext)
if (!context) {
throw new Error('useDropzoneContext must be used within a Dropzone')
}
return context
}
export type DropzoneContentProps = {
children?: ReactNode
className?: string
}
const maxLabelItems = 3
export const DropzoneContent = ({ children, className }: DropzoneContentProps) => {
const { src } = useDropzoneContext()
if (!src) {
return null
}
if (children) {
return children
}
return (
<div className={cn('flex flex-col items-center justify-center', className)}>
<div className="flex size-8 items-center justify-center rounded-md bg-muted text-muted-foreground">
<UploadIcon size={16} />
</div>
<p className="my-2 w-full truncate font-medium text-sm">
{src.length > maxLabelItems
? `${new Intl.ListFormat('en').format(
src.slice(0, maxLabelItems).map((file) => file.name)
)} and ${src.length - maxLabelItems} more`
: new Intl.ListFormat('en').format(src.map((file) => file.name))}
</p>
<p className="w-full text-wrap text-muted-foreground text-xs">Drag and drop or click to replace</p>
</div>
)
}
export type DropzoneEmptyStateProps = {
children?: ReactNode
className?: string
}
export const DropzoneEmptyState = ({ children, className }: DropzoneEmptyStateProps) => {
const { src, accept, maxSize, minSize, maxFiles } = useDropzoneContext()
if (src) {
return null
}
if (children) {
return children
}
let caption = ''
if (accept) {
caption += 'Accepts '
caption += new Intl.ListFormat('en').format(Object.keys(accept))
}
if (minSize && maxSize) {
caption += ` between ${renderBytes(minSize)} and ${renderBytes(maxSize)}`
} else if (minSize) {
caption += ` at least ${renderBytes(minSize)}`
} else if (maxSize) {
caption += ` less than ${renderBytes(maxSize)}`
}
return (
<div className={cn('flex flex-col items-center justify-center', className)}>
<div className="flex size-8 items-center justify-center rounded-md bg-muted text-muted-foreground">
<UploadIcon size={16} />
</div>
<p className="my-2 w-full truncate text-wrap font-medium text-sm">Upload {maxFiles === 1 ? 'a file' : 'files'}</p>
<p className="w-full truncate text-wrap text-muted-foreground text-xs">Drag and drop or click to upload</p>
{caption && <p className="text-wrap text-muted-foreground text-xs">{caption}.</p>}
</div>
)
}

View File

@@ -1,37 +0,0 @@
// Original: src/renderer/src/components/Spinner.tsx
import { motion } from 'framer-motion'
import { Search } from 'lucide-react'
interface Props {
text: React.ReactNode
className?: string
}
// Define variants for the spinner animation
const spinnerVariants = {
defaultColor: {
color: '#2a2a2a'
},
dimmed: {
color: '#8C9296'
}
}
export default function Spinner({ text, className = '' }: Props) {
return (
<motion.div
className={`flex items-center gap-1 p-0 ${className}`}
variants={spinnerVariants}
initial="defaultColor"
animate={['defaultColor', 'dimmed']}
transition={{
duration: 0.8,
repeat: Infinity,
repeatType: 'reverse',
ease: 'easeInOut'
}}>
<Search size={16} style={{ color: 'unset' }} />
<span>{text}</span>
</motion.div>
)
}

View File

@@ -1,54 +0,0 @@
import type { SwitchProps } from '@heroui/react'
import { cn, Spinner, Switch } from '@heroui/react'
// Enhanced Switch component with loading state support
interface CustomSwitchProps extends SwitchProps {
isLoading?: boolean
}
/**
* A customized Switch component based on HeroUI Switch
* @see https://www.heroui.com/docs/components/switch#api
* @param isLoading When true, displays a loading spinner in the switch thumb
*/
const CustomizedSwitch = ({ isLoading, children, ref, thumbIcon, ...props }: CustomSwitchProps) => {
const finalThumbIcon = isLoading ? <Spinner size="sm" /> : thumbIcon
return (
<Switch ref={ref} {...props} thumbIcon={finalThumbIcon}>
{children}
</Switch>
)
}
const DescriptionSwitch = ({ children, ...props }: CustomSwitchProps) => {
return (
<CustomizedSwitch
size="sm"
classNames={{
base: cn(
'inline-flex w-full max-w-md flex-row-reverse items-center hover:bg-content2',
'cursor-pointer justify-between gap-2 rounded-lg border-2 border-transparent py-2 pr-1',
'data-[selected=true]:border-primary'
),
wrapper: 'p-0 h-4 overflow-visible',
thumb: cn(
'h-6 w-6 border-2 shadow-lg',
'group-data-[hover=true]:border-primary',
//selected
'group-data-[selected=true]:ms-6',
// pressed
'group-data-[pressed=true]:w-7',
'group-data-pressed:group-data-selected:ms-4'
)
}}
{...props}>
{children}
</CustomizedSwitch>
)
}
CustomizedSwitch.displayName = 'Switch'
export { DescriptionSwitch, CustomizedSwitch as Switch }
export type { CustomSwitchProps as SwitchProps }

View File

@@ -1,34 +0,0 @@
import type { TooltipProps as HeroUITooltipProps } from '@heroui/react'
import { cn, Tooltip as HeroUITooltip } from '@heroui/react'
export interface TooltipProps extends HeroUITooltipProps {}
/**
* Tooltip wrapper that applies consistent styling and arrow display.
* Differences from raw HeroUI Tooltip:
* 1. Defaults showArrow={true}
* 2. Merges a default max-w-60 class into the content slot, capping width at 240px.
* All other HeroUI Tooltip props/behaviors remain unchanged.
*
* @see https://www.heroui.com/docs/components/tooltip
*/
export const Tooltip = ({
children,
classNames,
showArrow,
...rest
}: Omit<TooltipProps, 'classNames'> & {
classNames?: TooltipProps['classNames'] & { placeholder?: string }
}) => {
return (
<HeroUITooltip
classNames={{
...classNames,
content: cn('max-w-60', classNames?.content)
}}
showArrow={showArrow ?? true}
{...rest}>
<div className={cn('relative z-10 inline-block', classNames?.placeholder)}>{children}</div>
</HeroUITooltip>
)
}

View File

@@ -1,2 +0,0 @@
export * from './useDndReorder'
export * from './useDndState'

View File

@@ -1,5 +0,0 @@
// 主入口文件 - 导出所有公共API
export * from './components'
export * from './hooks'
// export * from './types'
export * from './utils'

Some files were not shown because too many files have changed in this diff Show More