Compare commits
4 Commits
refactor/o
...
feat/show-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d8b841c386 | ||
|
|
5ee120db7c | ||
|
|
c22034080c | ||
|
|
b6fa2583d1 |
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
@@ -1,5 +1,4 @@
|
||||
/src/renderer/src/store/ @0xfullex
|
||||
/src/renderer/src/databases/ @0xfullex
|
||||
/src/main/services/ConfigManager.ts @0xfullex
|
||||
/packages/shared/IpcChannel.ts @0xfullex
|
||||
/src/main/ipc.ts @0xfullex
|
||||
@@ -10,4 +9,3 @@
|
||||
/src/renderer/src/data/ @0xfullex
|
||||
|
||||
/packages/ui/ @MyPrototypeWhat
|
||||
|
||||
|
||||
12
.github/pull_request_template.md
vendored
12
.github/pull_request_template.md
vendored
@@ -3,18 +3,6 @@
|
||||
1. Consider creating this PR as draft: https://github.com/CherryHQ/cherry-studio/blob/main/CONTRIBUTING.md
|
||||
-->
|
||||
|
||||
<!--
|
||||
|
||||
⚠️ Important: Redux/IndexedDB Data-Changing Feature PRs Temporarily On Hold ⚠️
|
||||
|
||||
Please note: For our current development cycle, we are not accepting feature Pull Requests that introduce changes to Redux data models or IndexedDB schemas.
|
||||
|
||||
While we value your contributions, PRs of this nature will be blocked without merge. We welcome all other contributions (bug fixes, perf enhancements, docs, etc.). Thank you!
|
||||
|
||||
Once version 2.0.0 is released, we will resume reviewing feature PRs.
|
||||
|
||||
-->
|
||||
|
||||
### What this PR does
|
||||
|
||||
Before this PR:
|
||||
|
||||
10
.github/workflows/auto-i18n.yml
vendored
10
.github/workflows/auto-i18n.yml
vendored
@@ -1,10 +1,9 @@
|
||||
name: Auto I18N
|
||||
|
||||
env:
|
||||
TRANSLATION_API_KEY: ${{ secrets.TRANSLATE_API_KEY }}
|
||||
TRANSLATION_MODEL: ${{ vars.AUTO_I18N_MODEL || 'deepseek/deepseek-v3.1'}}
|
||||
TRANSLATION_BASE_URL: ${{ vars.AUTO_I18N_BASE_URL || 'https://api.ppinfra.com/openai'}}
|
||||
TRANSLATION_BASE_LOCALE: ${{ vars.AUTO_I18N_BASE_LOCALE || 'en-us'}}
|
||||
API_KEY: ${{ secrets.TRANSLATE_API_KEY }}
|
||||
MODEL: ${{ vars.AUTO_I18N_MODEL || 'deepseek/deepseek-v3.1'}}
|
||||
BASE_URL: ${{ vars.AUTO_I18N_BASE_URL || 'https://api.ppinfra.com/openai'}}
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
@@ -30,7 +29,6 @@ jobs:
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: 20
|
||||
package-manager-cache: false
|
||||
|
||||
- name: 📦 Install dependencies in isolated directory
|
||||
run: |
|
||||
@@ -44,7 +42,7 @@ jobs:
|
||||
echo "NODE_PATH=/tmp/translation-deps/node_modules" >> $GITHUB_ENV
|
||||
|
||||
- name: 🏃♀️ Translate
|
||||
run: npx tsx scripts/sync-i18n.ts && npx tsx scripts/auto-translate-i18n.ts
|
||||
run: npx tsx scripts/auto-translate-i18n.ts
|
||||
|
||||
- name: 🔍 Format
|
||||
run: cd /tmp/translation-deps && npx biome format --config-path /home/runner/work/cherry-studio/cherry-studio/biome.jsonc --write /home/runner/work/cherry-studio/cherry-studio/src/renderer/src/i18n/
|
||||
|
||||
@@ -1,131 +0,0 @@
|
||||
diff --git a/dist/index.mjs b/dist/index.mjs
|
||||
index b3f018730a93639aad7c203f15fb1aeb766c73f4..ade2a43d66e9184799d072153df61ef7be4ea110 100644
|
||||
--- a/dist/index.mjs
|
||||
+++ b/dist/index.mjs
|
||||
@@ -296,7 +296,14 @@ var HuggingFaceResponsesLanguageModel = class {
|
||||
metadata: huggingfaceOptions == null ? void 0 : huggingfaceOptions.metadata,
|
||||
instructions: huggingfaceOptions == null ? void 0 : huggingfaceOptions.instructions,
|
||||
...preparedTools && { tools: preparedTools },
|
||||
- ...preparedToolChoice && { tool_choice: preparedToolChoice }
|
||||
+ ...preparedToolChoice && { tool_choice: preparedToolChoice },
|
||||
+ ...(huggingfaceOptions?.reasoningEffort != null && {
|
||||
+ reasoning: {
|
||||
+ ...(huggingfaceOptions?.reasoningEffort != null && {
|
||||
+ effort: huggingfaceOptions.reasoningEffort,
|
||||
+ }),
|
||||
+ },
|
||||
+ }),
|
||||
};
|
||||
return { args: baseArgs, warnings };
|
||||
}
|
||||
@@ -365,6 +372,20 @@ var HuggingFaceResponsesLanguageModel = class {
|
||||
}
|
||||
break;
|
||||
}
|
||||
+ case 'reasoning': {
|
||||
+ for (const contentPart of part.content) {
|
||||
+ content.push({
|
||||
+ type: 'reasoning',
|
||||
+ text: contentPart.text,
|
||||
+ providerMetadata: {
|
||||
+ huggingface: {
|
||||
+ itemId: part.id,
|
||||
+ },
|
||||
+ },
|
||||
+ });
|
||||
+ }
|
||||
+ break;
|
||||
+ }
|
||||
case "mcp_call": {
|
||||
content.push({
|
||||
type: "tool-call",
|
||||
@@ -519,6 +540,11 @@ var HuggingFaceResponsesLanguageModel = class {
|
||||
id: value.item.call_id,
|
||||
toolName: value.item.name
|
||||
});
|
||||
+ } else if (value.item.type === 'reasoning') {
|
||||
+ controller.enqueue({
|
||||
+ type: 'reasoning-start',
|
||||
+ id: value.item.id,
|
||||
+ });
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -570,6 +596,22 @@ var HuggingFaceResponsesLanguageModel = class {
|
||||
});
|
||||
return;
|
||||
}
|
||||
+ if (isReasoningDeltaChunk(value)) {
|
||||
+ controller.enqueue({
|
||||
+ type: 'reasoning-delta',
|
||||
+ id: value.item_id,
|
||||
+ delta: value.delta,
|
||||
+ });
|
||||
+ return;
|
||||
+ }
|
||||
+
|
||||
+ if (isReasoningEndChunk(value)) {
|
||||
+ controller.enqueue({
|
||||
+ type: 'reasoning-end',
|
||||
+ id: value.item_id,
|
||||
+ });
|
||||
+ return;
|
||||
+ }
|
||||
},
|
||||
flush(controller) {
|
||||
controller.enqueue({
|
||||
@@ -593,7 +635,8 @@ var HuggingFaceResponsesLanguageModel = class {
|
||||
var huggingfaceResponsesProviderOptionsSchema = z2.object({
|
||||
metadata: z2.record(z2.string(), z2.string()).optional(),
|
||||
instructions: z2.string().optional(),
|
||||
- strictJsonSchema: z2.boolean().optional()
|
||||
+ strictJsonSchema: z2.boolean().optional(),
|
||||
+ reasoningEffort: z2.string().optional(),
|
||||
});
|
||||
var huggingfaceResponsesResponseSchema = z2.object({
|
||||
id: z2.string(),
|
||||
@@ -727,12 +770,31 @@ var responseCreatedChunkSchema = z2.object({
|
||||
model: z2.string()
|
||||
})
|
||||
});
|
||||
+var reasoningTextDeltaChunkSchema = z2.object({
|
||||
+ type: z2.literal('response.reasoning_text.delta'),
|
||||
+ item_id: z2.string(),
|
||||
+ output_index: z2.number(),
|
||||
+ content_index: z2.number(),
|
||||
+ delta: z2.string(),
|
||||
+ sequence_number: z2.number(),
|
||||
+});
|
||||
+
|
||||
+var reasoningTextEndChunkSchema = z2.object({
|
||||
+ type: z2.literal('response.reasoning_text.done'),
|
||||
+ item_id: z2.string(),
|
||||
+ output_index: z2.number(),
|
||||
+ content_index: z2.number(),
|
||||
+ text: z2.string(),
|
||||
+ sequence_number: z2.number(),
|
||||
+});
|
||||
var huggingfaceResponsesChunkSchema = z2.union([
|
||||
responseOutputItemAddedSchema,
|
||||
responseOutputItemDoneSchema,
|
||||
textDeltaChunkSchema,
|
||||
responseCompletedChunkSchema,
|
||||
responseCreatedChunkSchema,
|
||||
+ reasoningTextDeltaChunkSchema,
|
||||
+ reasoningTextEndChunkSchema,
|
||||
z2.object({ type: z2.string() }).loose()
|
||||
// fallback for unknown chunks
|
||||
]);
|
||||
@@ -751,6 +813,12 @@ function isResponseCompletedChunk(chunk) {
|
||||
function isResponseCreatedChunk(chunk) {
|
||||
return chunk.type === "response.created";
|
||||
}
|
||||
+function isReasoningDeltaChunk(chunk) {
|
||||
+ return chunk.type === 'response.reasoning_text.delta';
|
||||
+}
|
||||
+function isReasoningEndChunk(chunk) {
|
||||
+ return chunk.type === 'response.reasoning_text.done';
|
||||
+}
|
||||
|
||||
// src/huggingface-provider.ts
|
||||
function createHuggingFace(options = {}) {
|
||||
@@ -65,28 +65,7 @@ The Test Plan aims to provide users with a more stable application experience an
|
||||
### Other Suggestions
|
||||
|
||||
- **Contact Developers**: Before submitting a PR, you can contact the developers first to discuss or get help.
|
||||
|
||||
## Important Contribution Guidelines & Focus Areas
|
||||
|
||||
Please review the following critical information before submitting your Pull Request:
|
||||
|
||||
### Temporary Restriction on Data-Changing Feature PRs 🚫
|
||||
|
||||
**Currently, we are NOT accepting feature Pull Requests that introduce changes to our Redux data models or IndexedDB schemas.**
|
||||
|
||||
Our core team is currently focused on significant architectural updates that involve these data structures. To ensure stability and focus during this period, contributions of this nature will be temporarily managed internally.
|
||||
|
||||
* **PRs that require changes to Redux state shape or IndexedDB schemas will be closed.**
|
||||
* **This restriction is temporary and will be lifted with the release of `v2.0.0`.** You can track the progress of `v2.0.0` and its related discussions on issue [#10162](https://github.com/YOUR_ORG/YOUR_REPO/issues/10162) (please replace with your actual repo link).
|
||||
|
||||
We highly encourage contributions for:
|
||||
* Bug fixes 🐞
|
||||
* Performance improvements 🚀
|
||||
* Documentation updates 📚
|
||||
* Features that **do not** alter Redux data models or IndexedDB schemas (e.g., UI enhancements, new components, minor refactors). ✨
|
||||
|
||||
We appreciate your understanding and continued support during this important development phase. Thank you!
|
||||
|
||||
- **Become a Core Developer**: If you contribute to the project consistently, congratulations, you can become a core developer and gain project membership status. Please check our [Membership Guide](https://github.com/CherryHQ/community/blob/main/docs/membership.en.md).
|
||||
|
||||
## Contact Us
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
<p align="center">English | <a href="./docs/README.zh.md">中文</a> | <a href="https://cherry-ai.com">Official Site</a> | <a href="https://docs.cherry-ai.com/cherry-studio-wen-dang/en-us">Documents</a> | <a href="./docs/dev.md">Development</a> | <a href="https://github.com/CherryHQ/cherry-studio/issues">Feedback</a><br></p>
|
||||
|
||||
<div align="center">
|
||||
|
||||
|
||||
[![][deepwiki-shield]][deepwiki-link]
|
||||
[![][twitter-shield]][twitter-link]
|
||||
[![][discord-shield]][discord-link]
|
||||
@@ -45,7 +45,7 @@
|
||||
|
||||
</div>
|
||||
<div align="center">
|
||||
|
||||
|
||||
[![][github-release-shield]][github-release-link]
|
||||
[![][github-nightly-shield]][github-nightly-link]
|
||||
[![][github-contributors-shield]][github-contributors-link]
|
||||
@@ -248,10 +248,10 @@ The Enterprise Edition addresses core challenges in team collaboration by centra
|
||||
|
||||
| Feature | Community Edition | Enterprise Edition |
|
||||
| :---------------- | :----------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Open Source** | ✅ Yes | ⭕️ Partially released to customers |
|
||||
| **Open Source** | ✅ Yes | ⭕️ Partially released to customers |
|
||||
| **Cost** | Free for Personal Use / Commercial License | Buyout / Subscription Fee |
|
||||
| **Admin Backend** | — | ● Centralized **Model** Access<br>● **Employee** Management<br>● Shared **Knowledge Base**<br>● **Access** Control<br>● **Data** Backup |
|
||||
| **Server** | — | ✅ Dedicated Private Deployment |
|
||||
| **Server** | — | ✅ Dedicated Private Deployment |
|
||||
|
||||
## Get the Enterprise Edition
|
||||
|
||||
|
||||
@@ -69,28 +69,7 @@ git commit --signoff -m "Your commit message"
|
||||
### 其他建议
|
||||
|
||||
- **联系开发者**:在提交 PR 之前,您可以先和开发者进行联系,共同探讨或者获取帮助。
|
||||
|
||||
## 重要贡献指南与关注点
|
||||
|
||||
在提交 Pull Request 之前,请务必阅读以下关键信息:
|
||||
|
||||
### 🚫 暂时限制涉及数据更改的功能性 PR
|
||||
|
||||
**目前,我们不接受涉及 Redux 数据模型或 IndexedDB schema 变更的功能性 Pull Request。**
|
||||
|
||||
我们的核心团队目前正专注于涉及这些数据结构的关键架构更新和基础工作。为确保在此期间的稳定性与专注,此类贡献将暂时由内部进行管理。
|
||||
|
||||
* **需要更改 Redux 状态结构或 IndexedDB schema 的 PR 将会被关闭。**
|
||||
* **此限制是临时性的,并将在 `v2.0.0` 版本发布后解除。** 您可以通过 Issue [#10162](https://github.com/YOUR_ORG/YOUR_REPO/issues/10162) (请替换为您的实际仓库链接) 跟踪 `v2.0.0` 的进展及相关讨论。
|
||||
|
||||
我们非常鼓励以下类型的贡献:
|
||||
* 错误修复 🐞
|
||||
* 性能改进 🚀
|
||||
* 文档更新 📚
|
||||
* 不改变 Redux 数据模型或 IndexedDB schema 的功能(例如,UI 增强、新组件、小型重构)。✨
|
||||
|
||||
感谢您在此重要开发阶段的理解与持续支持。谢谢!
|
||||
|
||||
- **成为核心开发者**:如果您能够稳定为项目贡献,恭喜您可以成为项目核心开发者,获取到项目成员身份。请查看我们的[成员指南](https://github.com/CherryHQ/community/blob/main/membership.md)
|
||||
|
||||
## 联系我们
|
||||
|
||||
|
||||
@@ -1,260 +0,0 @@
|
||||
> [!NOTE]
|
||||
> This technical documentation was automatically generated by Claude Code based on analysis of the current OCR implementation in the codebase. The content reflects the architecture as of the current branch state.
|
||||
|
||||
# OCR Architecture
|
||||
|
||||
## Overview
|
||||
|
||||
Cherry Studio's OCR (Optical Character Recognition) system is a modular, extensible architecture designed to support multiple OCR providers and file types. The architecture follows a layered approach with clear separation of concerns between data access, business logic, and provider implementations.
|
||||
|
||||
## Architecture Layers
|
||||
|
||||
The OCR architecture follows a layered approach where data interactions occur through RESTful APIs, while IPC serves as part of the API layer, allowing the renderer to interact directly with the business layer:
|
||||
|
||||
### 1. API Layer
|
||||
**Location**: `src/main/data/api/handlers/`, `src/main/ipc.ts`, `src/preload/index.ts`
|
||||
|
||||
- **IPC Bridge**: Serves as API layer connecting renderer to main process
|
||||
- **Request Routing**: Routes IPC calls to appropriate service methods
|
||||
- **Type Safety**: Zod schemas for request/response validation
|
||||
- **Error Handling**: Centralized error propagation across process boundaries
|
||||
- **Security**: Secure communication sandbox between renderer and main processes
|
||||
|
||||
### 2. OCR Service Layer (Business Layer)
|
||||
**Location**: `src/main/services/ocr/`
|
||||
|
||||
- **OcrService**: Main business logic orchestrator and central coordinator
|
||||
- **Provider Registry**: Manages registered OCR providers
|
||||
- **Data Integration**: Direct interaction with data layer for provider management
|
||||
- **Lifecycle Management**: Handles provider initialization and disposal
|
||||
- **Validation**: Ensures provider availability and data integrity
|
||||
- **Orchestration**: Coordinates between providers and data services
|
||||
- **Direct IPC Access**: Renderer can directly invoke business layer methods via IPC
|
||||
|
||||
### 3. Provider Services Layer
|
||||
**Location**: `src/main/services/ocr/builtin/`
|
||||
|
||||
- **Base Service**: Abstract `OcrBaseService` defines common interface
|
||||
- **Data Independence**: No direct database interactions, relies on injected data
|
||||
- **Built-in Providers**:
|
||||
- `TesseractService`: Local Tesseract.js implementation
|
||||
- `SystemOcrService`: Platform-specific system OCR
|
||||
- `PpocrService`: PaddleOCR integration
|
||||
- `OvOcrService`: Intel OpenVINO (NPU) OCR
|
||||
- **Pure OCR Logic**: Focus solely on OCR processing capabilities
|
||||
|
||||
### 4. Data Layer
|
||||
**Location**: `src/main/data/db/schemas/ocr/`, `src/main/data/repositories/`
|
||||
|
||||
- **Database Schema**: Uses Drizzle ORM with SQLite database
|
||||
- **Repository Pattern**: `OcrProviderRepository` handles all database operations
|
||||
- **Provider Storage**: Stores provider configurations in `ocr_provider` table
|
||||
- **JSON Configuration**: Polymorphic `config` field stores provider-specific settings
|
||||
- **Data Access**: Exclusively accessed by OCR Service layer
|
||||
|
||||
### 5. Frontend Layer
|
||||
**Location**: `src/renderer/src/services/ocr/`, `src/renderer/src/hooks/ocr/`
|
||||
|
||||
- **Direct IPC Communication**: Direct interaction with business layer via IPC
|
||||
- **React Hooks**: Custom hooks for OCR operations and state management
|
||||
- **Configuration UI**: Settings pages for provider configuration
|
||||
- **State Management**: Frontend state synchronization with backend data
|
||||
|
||||
## Data Flow
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[Frontend UI] --> B[Frontend OCR Service]
|
||||
B --> C[API Layer - IPC Bridge]
|
||||
C --> D[OCR Service Layer - Business Logic]
|
||||
D --> E[Data Layer - Provider Repository]
|
||||
D --> F[Provider Services Layer]
|
||||
F --> G[OCR Processing]
|
||||
G --> H[Result]
|
||||
H --> F
|
||||
F --> D
|
||||
D --> C
|
||||
C --> B
|
||||
B --> A
|
||||
|
||||
style D fill:#e1f5fe
|
||||
style F fill:#f3e5f5
|
||||
style E fill:#e8f5e8
|
||||
style C fill:#fff3e0
|
||||
```
|
||||
|
||||
**Key Flow Characteristics:**
|
||||
- **Direct Business Access**: Frontend communicates directly with OCR Service layer via IPC
|
||||
- **IPC as API Gateway**: IPC bridge functions as the API layer, handling routing and validation
|
||||
- **Data Isolation**: Only business layer interacts with data persistence
|
||||
- **Provider Independence**: OCR providers remain isolated from data concerns
|
||||
|
||||
## Provider System
|
||||
|
||||
### Provider Registration
|
||||
- **Built-in Providers**: Automatically registered on service initialization
|
||||
- **Custom Providers**: Support for extensible provider system
|
||||
- **Configuration**: Each provider has its own configuration schema
|
||||
|
||||
### Provider Capabilities
|
||||
```typescript
|
||||
interface OcrProviderCapabilityRecord {
|
||||
image?: boolean // Image file OCR support
|
||||
pdf?: boolean // PDF file OCR support (future)
|
||||
}
|
||||
```
|
||||
|
||||
### Configuration Architecture
|
||||
- **Polymorphic Config**: JSON-based configuration adapts to provider needs
|
||||
- **Type Safety**: Zod schemas validate provider-specific configurations
|
||||
- **Runtime Validation**: Configuration validation before OCR operations
|
||||
|
||||
## Type System
|
||||
|
||||
### Core Types
|
||||
- **`OcrProvider`**: Base provider interface
|
||||
- **`OcrParams`**: OCR operation parameters
|
||||
- **`OcrResult`**: Standardized OCR result format
|
||||
- **`SupportedOcrFile`**: File types supported for OCR
|
||||
|
||||
### Business Types
|
||||
- **`OcrProviderBusiness`**: Domain-level provider representation
|
||||
- **Operations**: Create, Update, Replace, Delete operations
|
||||
- **Queries**: List providers with filtering options
|
||||
|
||||
### Provider-Specific Types
|
||||
- **TesseractConfig**: Language selection, model paths
|
||||
- **SystemOcrConfig**: Language preferences
|
||||
- **PaddleOCRConfig**: API endpoints, authentication
|
||||
- **OpenVINOConfig**: Device selection, model paths
|
||||
|
||||
## Built-in Providers
|
||||
|
||||
### Tesseract OCR
|
||||
- **Engine**: Tesseract.js
|
||||
- **Languages**: Multi-language support with automatic download
|
||||
- **Configuration**: Language selection, cache management
|
||||
- **Performance**: Worker pooling for concurrent processing
|
||||
|
||||
### System OCR
|
||||
- **Windows**: Windows Media Foundation OCR
|
||||
- **macOS**: Vision framework OCR
|
||||
- **Linux**: Platform-specific implementations
|
||||
- **Features**: Native performance, system integration
|
||||
|
||||
### PaddleOCR
|
||||
- **Deployment**: Remote API integration
|
||||
- **Languages**: Chinese, English, and mixed language support
|
||||
- **Configuration**: API endpoints and authentication
|
||||
|
||||
### Intel OpenVINO OCR
|
||||
- **Hardware**: NPU acceleration support
|
||||
- **Performance**: Optimized for Intel hardware
|
||||
- **Use Case**: High-performance OCR scenarios
|
||||
|
||||
## Configuration Management
|
||||
|
||||
### Database Schema
|
||||
```sql
|
||||
CREATE TABLE ocr_provider (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
capabilities TEXT NOT NULL, -- JSON
|
||||
config TEXT NOT NULL, -- JSON
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL
|
||||
);
|
||||
```
|
||||
|
||||
### Provider Defaults
|
||||
- **Initial Configuration**: Defined in `packages/shared/config/ocr.ts`
|
||||
- **Migration System**: Automatic provider initialization on startup
|
||||
- **User Customization**: Runtime configuration updates
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Error Categories
|
||||
- **Provider Errors**: OCR engine failures, missing dependencies
|
||||
- **Configuration Errors**: Invalid settings, missing parameters
|
||||
- **File Errors**: Unsupported formats, corrupted files
|
||||
- **System Errors**: Resource exhaustion, permissions
|
||||
|
||||
### Error Propagation
|
||||
- **Logging**: Centralized logging with context
|
||||
- **User Feedback**: Translated error messages
|
||||
- **Recovery**: Graceful fallback options
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### Resource Management
|
||||
- **Worker Disposal**: Proper cleanup of OCR workers
|
||||
- **Memory Management**: Limits on file sizes and concurrent operations
|
||||
- **Caching**: Model and result caching where applicable
|
||||
|
||||
### Optimization
|
||||
- **Lazy Loading**: Providers initialized on demand
|
||||
- **Concurrent Processing**: Multiple workers for parallel operations
|
||||
- **Hardware Acceleration**: NPU and GPU support where available
|
||||
|
||||
## Security
|
||||
|
||||
### Input Validation
|
||||
- **File Type Checking**: Strict validation of supported formats
|
||||
- **Size Limits**: Protection against resource exhaustion
|
||||
- **Path Validation**: Prevention of path traversal attacks
|
||||
|
||||
### Configuration Security
|
||||
- **API Key Storage**: Secure storage of sensitive configuration
|
||||
- **Validation**: Runtime validation of configuration parameters
|
||||
- **Sandboxing**: Isolated execution of OCR operations
|
||||
|
||||
## Extension Points
|
||||
|
||||
### Custom Providers
|
||||
- **Interface**: Implement `OcrBaseService` for new providers
|
||||
- **Registration**: Dynamic provider registration system
|
||||
- **Configuration**: Extensible configuration schemas
|
||||
|
||||
### File Type Support
|
||||
- **Handlers**: Modular file type processors
|
||||
- **Capabilities**: Declarative provider capabilities
|
||||
- **Future Support**: PDF, document formats planned
|
||||
|
||||
## Migration Strategy
|
||||
|
||||
### Legacy System
|
||||
- **Data Migration**: Automatic migration from old configuration formats
|
||||
- **Compatibility**: Backward compatibility during transition
|
||||
- **Testing**: Comprehensive test coverage for migration paths
|
||||
|
||||
### Future Enhancements
|
||||
- **PDF Support**: Planned extension to document OCR
|
||||
- **Cloud Providers**: API-based OCR services integration
|
||||
- **AI Enhancement**: Post-processing and accuracy improvements
|
||||
|
||||
## Development Guidelines
|
||||
|
||||
### Adding New Providers
|
||||
1. Create provider service extending `OcrBaseService`
|
||||
2. Define provider-specific configuration schema
|
||||
3. Register provider in `OcrService`
|
||||
4. Add configuration UI components
|
||||
5. Include comprehensive tests
|
||||
|
||||
> [!WARNING]
|
||||
> Provider services should never directly access the data layer. All data operations must go through the OCR Service layer to maintain proper separation of concerns.
|
||||
|
||||
### Configuration Changes
|
||||
1. Update provider configuration schema
|
||||
2. Add migration logic for existing configurations
|
||||
3. Update UI validation and error handling
|
||||
4. Test with various configuration scenarios
|
||||
|
||||
> [!WARNING]
|
||||
> Always validate configuration changes before saving to the database. Use Zod schemas for runtime validation to prevent corrupted provider configurations.
|
||||
|
||||
### Testing
|
||||
- **Unit Tests**: Provider implementation testing
|
||||
- **Integration Tests**: End-to-end OCR workflows
|
||||
- **Performance Tests**: Resource usage and timing
|
||||
- **Error Scenarios**: Comprehensive error handling testing
|
||||
@@ -1,260 +0,0 @@
|
||||
> [!NOTE]
|
||||
> 本技术文档由 Claude Code 基于对当前代码库中 OCR 实现的分析自动生成。内容反映了当前分支状态的架构设计。
|
||||
|
||||
# OCR 架构文档
|
||||
|
||||
## 概述
|
||||
|
||||
Cherry Studio 的 OCR(光学字符识别)系统是一个模块化、可扩展的架构,旨在支持多个 OCR 提供商和文件类型。该架构采用分层设计,在数据访问、业务逻辑和提供商实现之间有明确的关注点分离。
|
||||
|
||||
## 架构分层
|
||||
|
||||
OCR 架构采用分层方法,其中数据交互通过 RESTful API 进行,而 IPC 作为 API 层的一部分,允许 Renderer 直接与业务层交互:
|
||||
|
||||
### 1. API 层
|
||||
**位置**: `src/main/data/api/handlers/`, `src/main/ipc.ts`, `src/preload/index.ts`
|
||||
|
||||
- **IPC 桥接**: 作为 API 层连接 Renderer 到主进程
|
||||
- **请求路由**: 将 IPC 调用路由到相应的服务方法
|
||||
- **类型安全**: 使用 Zod 模式进行请求/响应验证
|
||||
- **错误处理**: 跨进程边界的集中式错误传播
|
||||
- **安全**: Renderer 和主进程之间的安全通信沙盒
|
||||
|
||||
### 2. OCR 服务层(业务层)
|
||||
**位置**: `src/main/services/ocr/`
|
||||
|
||||
- **OcrService**: 主要业务逻辑协调器和中央协调器
|
||||
- **提供商注册表**: 管理已注册的 OCR 提供商
|
||||
- **数据集成**: 与数据层直接交互进行提供商管理
|
||||
- **生命周期管理**: 处理提供商初始化和销毁
|
||||
- **验证**: 确保提供商可用性和数据完整性
|
||||
- **协调**: 协调提供商和数据服务之间的交互
|
||||
- **直接 IPC 访问**: Renderer 可通过 IPC 直接调用业务层方法
|
||||
|
||||
### 3. 提供商服务层
|
||||
**位置**: `src/main/services/ocr/builtin/`
|
||||
|
||||
- **基础服务**: 抽象的 `OcrBaseService` 定义通用接口
|
||||
- **数据独立性**: 无直接数据库交互,依赖外部传入的数据
|
||||
- **内置提供商**:
|
||||
- `TesseractService`: 本地 Tesseract.js 实现
|
||||
- `SystemOcrService`: 平台特定的系统 OCR
|
||||
- `PpocrService`: PaddleOCR 集成
|
||||
- `OvOcrService`: Intel OpenVINO (NPU) OCR
|
||||
- **纯 OCR 逻辑**: 专注于 OCR 处理能力
|
||||
|
||||
### 4. 数据层
|
||||
**位置**: `src/main/data/db/schemas/ocr/`, `src/main/data/repositories/`
|
||||
|
||||
- **数据库架构**: 使用 Drizzle ORM 和 SQLite 数据库
|
||||
- **仓储模式**: `OcrProviderRepository` 处理所有数据库操作
|
||||
- **提供商存储**: 在 `ocr_provider` 表中存储提供商配置
|
||||
- **JSON 配置**: 多态的 `config` 字段存储提供商特定的设置
|
||||
- **数据访问**: 仅由 OCR 服务层访问
|
||||
|
||||
### 5. Renderer 层
|
||||
**位置**: `src/renderer/src/services/ocr/`, `src/renderer/src/hooks/ocr/`
|
||||
|
||||
- **直接 IPC 通信**: 通过 IPC 与业务层直接交互
|
||||
- **React Hooks**: 用于 OCR 操作和状态管理的自定义钩子
|
||||
- **配置 UI**: 提供商配置的设置页面
|
||||
- **状态管理**: Renderer 状态与后端数据同步
|
||||
|
||||
## 数据流
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[Renderer UI] --> B[Renderer OCR 服务]
|
||||
B --> C[API 层 - IPC 桥接]
|
||||
C --> D[OCR 服务层 - 业务逻辑]
|
||||
D --> E[数据层 - 提供商仓储]
|
||||
D --> F[提供商服务层]
|
||||
F --> G[OCR 处理]
|
||||
G --> H[结果]
|
||||
H --> F
|
||||
F --> D
|
||||
D --> C
|
||||
C --> B
|
||||
B --> A
|
||||
|
||||
style D fill:#e1f5fe
|
||||
style F fill:#f3e5f5
|
||||
style E fill:#e8f5e8
|
||||
style C fill:#fff3e0
|
||||
```
|
||||
|
||||
**关键流程特征**:
|
||||
- **直接业务访问**: Renderer 通过 IPC 与 OCR 服务层直接通信
|
||||
- **IPC 作为 API 网关**: IPC 桥接作为 API 层,处理路由和验证
|
||||
- **数据隔离**: 只有业务层与数据持久化交互
|
||||
- **提供商独立性**: OCR 提供商保持与数据关注点的隔离
|
||||
|
||||
## 提供商系统
|
||||
|
||||
### 提供商注册
|
||||
- **内置提供商**: 在服务初始化时自动注册
|
||||
- **自定义提供商**: 支持可扩展的提供商系统
|
||||
- **配置**: 每个提供商都有自己的配置模式
|
||||
|
||||
### 提供商能力
|
||||
```typescript
|
||||
interface OcrProviderCapabilityRecord {
|
||||
image?: boolean // 图像文件 OCR 支持
|
||||
pdf?: boolean // PDF 文件 OCR 支持(未来)
|
||||
}
|
||||
```
|
||||
|
||||
### 配置架构
|
||||
- **多态配置**: 基于 JSON 的配置适应提供商需求
|
||||
- **类型安全**: Zod 模式验证提供商特定的配置
|
||||
- **运行时验证**: OCR 操作前的配置验证
|
||||
|
||||
## 类型系统
|
||||
|
||||
### 核心类型
|
||||
- **`OcrProvider`**: 基础提供商接口
|
||||
- **`OcrParams`**: OCR 操作参数
|
||||
- **`OcrResult`**: 标准化的 OCR 结果格式
|
||||
- **`SupportedOcrFile`**: 支持 OCR 的文件类型
|
||||
|
||||
### 业务类型
|
||||
- **`OcrProviderBusiness`**: 域级别的提供商表示
|
||||
- **操作**: 创建、更新、替换、删除操作
|
||||
- **查询**: 带过滤选项的提供商列表
|
||||
|
||||
### 提供商特定类型
|
||||
- **TesseractConfig**: 语言选择、模型路径
|
||||
- **SystemOcrConfig**: 语言偏好
|
||||
- **PaddleOCRConfig**: API 端点、认证
|
||||
- **OpenVINOConfig**: 设备选择、模型路径
|
||||
|
||||
## 内置提供商
|
||||
|
||||
### Tesseract OCR
|
||||
- **引擎**: Tesseract.js
|
||||
- **语言**: 支持多语言,自动下载
|
||||
- **配置**: 语言选择、缓存管理
|
||||
- **性能**: 工作池用于并发处理
|
||||
|
||||
### 系统 OCR
|
||||
- **Windows**: Windows Media Foundation OCR
|
||||
- **macOS**: Vision 框架 OCR
|
||||
- **Linux**: 平台特定实现
|
||||
- **特性**: 原生性能、系统集成
|
||||
|
||||
### PaddleOCR
|
||||
- **部署**: 远程 API 集成
|
||||
- **语言**: 中文、英文和混合语言支持
|
||||
- **配置**: API 端点和认证
|
||||
|
||||
### Intel OpenVINO OCR
|
||||
- **硬件**: NPU 加速支持
|
||||
- **性能**: 为 Intel 硬件优化
|
||||
- **用例**: 高性能 OCR 场景
|
||||
|
||||
## 配置管理
|
||||
|
||||
### 数据库架构
|
||||
```sql
|
||||
CREATE TABLE ocr_provider (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
capabilities TEXT NOT NULL, -- JSON
|
||||
config TEXT NOT NULL, -- JSON
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL
|
||||
);
|
||||
```
|
||||
|
||||
### 提供商默认值
|
||||
- **初始配置**: 在 `packages/shared/config/ocr.ts` 中定义
|
||||
- **迁移系统**: 启动时自动提供商初始化
|
||||
- **用户自定义**: 运行时配置更新
|
||||
|
||||
## 错误处理
|
||||
|
||||
### 错误类别
|
||||
- **提供商错误**: OCR 引擎故障、缺少依赖
|
||||
- **配置错误**: 无效设置、缺少参数
|
||||
- **文件错误**: 不支持的格式、损坏的文件
|
||||
- **系统错误**: 资源耗尽、权限问题
|
||||
|
||||
### 错误传播
|
||||
- **日志**: 带上下文的集中日志记录
|
||||
- **用户反馈**: 翻译的错误消息
|
||||
- **恢复**: 优雅的回退选项
|
||||
|
||||
## 性能考虑
|
||||
|
||||
### 资源管理
|
||||
- **工作器销毁**: OCR 工作器的适当清理
|
||||
- **内存管理**: 文件大小和并发操作限制
|
||||
- **缓存**: 模型和结果缓存(如适用)
|
||||
|
||||
### 优化
|
||||
- **延迟加载**: 按需初始化提供商
|
||||
- **并发处理**: 多工作器用于并行操作
|
||||
- **硬件加速**: NPU 和 GPU 支持(如可用)
|
||||
|
||||
## 安全
|
||||
|
||||
### 输入验证
|
||||
- **文件类型检查**: 严格验证支持的格式
|
||||
- **大小限制**: 防止资源耗尽
|
||||
- **路径验证**: 防止路径遍历攻击
|
||||
|
||||
### 配置安全
|
||||
- **API 密钥存储**: 敏感配置的安全存储
|
||||
- **验证**: 配置参数的运行时验证
|
||||
- **沙盒**: OCR 操作的隔离执行
|
||||
|
||||
## 扩展点
|
||||
|
||||
### 自定义提供商
|
||||
- **接口**: 为新提供商实现 `OcrBaseService`
|
||||
- **注册**: 动态提供商注册系统
|
||||
- **配置**: 可扩展的配置模式
|
||||
|
||||
### 文件类型支持
|
||||
- **处理器**: 模块化文件类型处理器
|
||||
- **能力**: 声明式提供商能力
|
||||
- **未来支持**: PDF、文档格式计划中
|
||||
|
||||
## 迁移策略
|
||||
|
||||
### 遗留系统
|
||||
- **数据迁移**: 从旧配置格式自动迁移
|
||||
- **兼容性**: 过渡期间的向后兼容性
|
||||
- **测试**: 迁移路径的全面测试覆盖
|
||||
|
||||
### 未来增强
|
||||
- **PDF 支持**: 计划扩展到文档 OCR
|
||||
- **云提供商**: 基于 API 的 OCR 服务集成
|
||||
- **AI 增强**: 后处理和准确性改进
|
||||
|
||||
## 开发指南
|
||||
|
||||
### 添加新提供商
|
||||
1. 创建扩展 `OcrBaseService` 的提供商服务
|
||||
2. 定义提供商特定的配置模式
|
||||
3. 在 `OcrService` 中注册提供商
|
||||
4. 添加配置 UI 组件
|
||||
5. 包含全面的测试
|
||||
|
||||
> [!WARNING]
|
||||
> 提供商服务绝不应直接访问数据层。所有数据操作必须通过 OCR 服务层进行,以保持适当的关注点分离。
|
||||
|
||||
### 配置更改
|
||||
1. 更新提供商配置模式
|
||||
2. 为现有配置添加迁移逻辑
|
||||
3. 更新 UI 验证和错误处理
|
||||
4. 测试各种配置场景
|
||||
|
||||
> [!WARNING]
|
||||
> 在保存到数据库之前,务必验证配置更改。使用 Zod 模式进行运行时验证,防止提供商配置损坏。
|
||||
|
||||
### 测试
|
||||
- **单元测试**: 提供商实现测试
|
||||
- **集成测试**: 端到端 OCR 工作流
|
||||
- **性能测试**: 资源使用和时间
|
||||
- **错误场景**: 全面的错误处理测试
|
||||
@@ -1,10 +0,0 @@
|
||||
CREATE TABLE `ocr_provider` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`name` text NOT NULL,
|
||||
`capabilities` text NOT NULL,
|
||||
`config` text NOT NULL,
|
||||
`created_at` integer,
|
||||
`updated_at` integer
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX `name` ON `ocr_provider` (`name`);
|
||||
@@ -1,172 +0,0 @@
|
||||
{
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "64f7ad88-7111-4574-988c-d7ef429e375d",
|
||||
"prevId": "de8009d7-95b9-4f99-99fa-4b8795708f21",
|
||||
"tables": {
|
||||
"app_state": {
|
||||
"name": "app_state",
|
||||
"columns": {
|
||||
"key": {
|
||||
"name": "key",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"value": {
|
||||
"name": "value",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"description": {
|
||||
"name": "description",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"ocr_provider": {
|
||||
"name": "ocr_provider",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"capabilities": {
|
||||
"name": "capabilities",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"config": {
|
||||
"name": "config",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"name": {
|
||||
"name": "name",
|
||||
"columns": ["name"],
|
||||
"isUnique": false
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"preference": {
|
||||
"name": "preference",
|
||||
"columns": {
|
||||
"scope": {
|
||||
"name": "scope",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"key": {
|
||||
"name": "key",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"value": {
|
||||
"name": "value",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"scope_name_idx": {
|
||||
"name": "scope_name_idx",
|
||||
"columns": ["scope", "key"],
|
||||
"isUnique": false
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
}
|
||||
},
|
||||
"views": {},
|
||||
"enums": {},
|
||||
"_meta": {
|
||||
"schemas": {},
|
||||
"tables": {},
|
||||
"columns": {}
|
||||
},
|
||||
"internal": {
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
||||
@@ -7,13 +7,6 @@
|
||||
"tag": "0000_solid_lord_hawal",
|
||||
"version": "6",
|
||||
"when": 1754745234572
|
||||
},
|
||||
{
|
||||
"idx": 1,
|
||||
"version": "6",
|
||||
"when": 1760969721294,
|
||||
"tag": "0001_previous_sir_ram",
|
||||
"breakpoints": true
|
||||
}
|
||||
],
|
||||
"version": "7"
|
||||
|
||||
21
package.json
21
package.json
@@ -85,7 +85,6 @@
|
||||
"@libsql/client": "0.14.0",
|
||||
"@libsql/win32-x64-msvc": "^0.4.7",
|
||||
"@napi-rs/system-ocr": "patch:@napi-rs/system-ocr@npm%3A1.0.2#~/.yarn/patches/@napi-rs-system-ocr-npm-1.0.2-59e7a78e8b.patch",
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@strongtz/win32-arm64-msvc": "^0.4.7",
|
||||
"express": "^5.1.0",
|
||||
"font-list": "^2.0.0",
|
||||
@@ -95,7 +94,7 @@
|
||||
"officeparser": "^4.2.0",
|
||||
"os-proxy-config": "^1.1.2",
|
||||
"selection-hook": "^1.0.12",
|
||||
"sharp": "0.34.4",
|
||||
"sharp": "^0.34.3",
|
||||
"swagger-jsdoc": "^6.2.8",
|
||||
"swagger-ui-express": "^5.0.1",
|
||||
"tesseract.js": "patch:tesseract.js@npm%3A6.0.1#~/.yarn/patches/tesseract.js-npm-6.0.1-2562a7e46d.patch",
|
||||
@@ -107,7 +106,6 @@
|
||||
"@agentic/tavily": "^7.3.3",
|
||||
"@ai-sdk/amazon-bedrock": "^3.0.35",
|
||||
"@ai-sdk/google-vertex": "^3.0.40",
|
||||
"@ai-sdk/huggingface": "patch:@ai-sdk/huggingface@npm%3A0.0.4#~/.yarn/patches/@ai-sdk-huggingface-npm-0.0.4-8080836bc1.patch",
|
||||
"@ai-sdk/mistral": "^2.0.19",
|
||||
"@ai-sdk/perplexity": "^2.0.13",
|
||||
"@ant-design/v5-patch-for-react-19": "^1.0.3",
|
||||
@@ -131,8 +129,8 @@
|
||||
"@cherrystudio/embedjs-ollama": "^0.1.31",
|
||||
"@cherrystudio/embedjs-openai": "^0.1.31",
|
||||
"@cherrystudio/extension-table-plus": "workspace:^",
|
||||
"@cherrystudio/openai": "^6.5.0",
|
||||
"@cherrystudio/ui": "workspace:*",
|
||||
"@cherrystudio/openai": "^6.5.0",
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/modifiers": "^9.0.0",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
@@ -153,7 +151,7 @@
|
||||
"@modelcontextprotocol/sdk": "^1.17.5",
|
||||
"@mozilla/readability": "^0.6.0",
|
||||
"@notionhq/client": "^2.2.15",
|
||||
"@openrouter/ai-sdk-provider": "^1.2.0",
|
||||
"@openrouter/ai-sdk-provider": "^1.1.2",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@opentelemetry/core": "2.0.0",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.200.0",
|
||||
@@ -391,13 +389,12 @@
|
||||
"vite": "npm:rolldown-vite@7.1.5",
|
||||
"tesseract.js@npm:*": "patch:tesseract.js@npm%3A6.0.1#~/.yarn/patches/tesseract.js-npm-6.0.1-2562a7e46d.patch",
|
||||
"@ai-sdk/google@npm:2.0.20": "patch:@ai-sdk/google@npm%3A2.0.20#~/.yarn/patches/@ai-sdk-google-npm-2.0.20-b9102f9d54.patch",
|
||||
"@img/sharp-darwin-arm64": "0.34.4",
|
||||
"@img/sharp-darwin-x64": "0.34.4",
|
||||
"@img/sharp-linux-arm": "0.34.4",
|
||||
"@img/sharp-linux-arm64": "0.34.4",
|
||||
"@img/sharp-linux-x64": "0.34.4",
|
||||
"@img/sharp-win32-x64": "0.34.4",
|
||||
"openai@npm:5.12.2": "npm:@cherrystudio/openai@6.5.0"
|
||||
"@img/sharp-darwin-arm64": "0.34.3",
|
||||
"@img/sharp-darwin-x64": "0.34.3",
|
||||
"@img/sharp-linux-arm": "0.34.3",
|
||||
"@img/sharp-linux-arm64": "0.34.3",
|
||||
"@img/sharp-linux-x64": "0.34.3",
|
||||
"@img/sharp-win32-x64": "0.34.3"
|
||||
},
|
||||
"packageManager": "yarn@4.9.1",
|
||||
"lint-staged": {
|
||||
|
||||
@@ -7,7 +7,6 @@ import { createAzure } from '@ai-sdk/azure'
|
||||
import { type AzureOpenAIProviderSettings } from '@ai-sdk/azure'
|
||||
import { createDeepSeek } from '@ai-sdk/deepseek'
|
||||
import { createGoogleGenerativeAI } from '@ai-sdk/google'
|
||||
import { createHuggingFace } from '@ai-sdk/huggingface'
|
||||
import { createOpenAI, type OpenAIProviderSettings } from '@ai-sdk/openai'
|
||||
import { createOpenAICompatible } from '@ai-sdk/openai-compatible'
|
||||
import type { LanguageModelV2 } from '@ai-sdk/provider'
|
||||
@@ -30,8 +29,7 @@ export const baseProviderIds = [
|
||||
'azure',
|
||||
'azure-responses',
|
||||
'deepseek',
|
||||
'openrouter',
|
||||
'huggingface'
|
||||
'openrouter'
|
||||
] as const
|
||||
|
||||
/**
|
||||
@@ -135,12 +133,6 @@ export const baseProviders = [
|
||||
name: 'OpenRouter',
|
||||
creator: createOpenRouter,
|
||||
supportsImageGeneration: true
|
||||
},
|
||||
{
|
||||
id: 'huggingface',
|
||||
name: 'HuggingFace',
|
||||
creator: createHuggingFace,
|
||||
supportsImageGeneration: true
|
||||
}
|
||||
] as const satisfies BaseProvider[]
|
||||
|
||||
|
||||
@@ -138,7 +138,6 @@ export enum IpcChannel {
|
||||
Windows_Close = 'window:close',
|
||||
Windows_IsMaximized = 'window:is-maximized',
|
||||
Windows_MaximizedChanged = 'window:maximized-changed',
|
||||
Windows_NavigateToAbout = 'window:navigate-to-about',
|
||||
|
||||
KnowledgeBase_Create = 'knowledge-base:create',
|
||||
KnowledgeBase_Reset = 'knowledge-base:reset',
|
||||
@@ -369,7 +368,8 @@ export enum IpcChannel {
|
||||
CodeTools_RemoveCustomTerminalPath = 'code-tools:remove-custom-terminal-path',
|
||||
|
||||
// OCR
|
||||
OCR_Ocr = 'ocr:ocr',
|
||||
OCR_ocr = 'ocr:ocr',
|
||||
OCR_ListProviders = 'ocr:list-providers',
|
||||
|
||||
// OVMS
|
||||
Ovms_AddModel = 'ovms:add-model',
|
||||
|
||||
@@ -1,176 +0,0 @@
|
||||
import type {
|
||||
BuiltinOcrProvider,
|
||||
BuiltinOcrProviderId,
|
||||
OcrOvProvider,
|
||||
OcrPpocrProvider,
|
||||
OcrSystemProvider,
|
||||
OcrTesseractProvider,
|
||||
TesseractLangCode
|
||||
} from '@types'
|
||||
|
||||
import type { TranslateLanguageCode } from '../../../src/renderer/src/types/translate'
|
||||
|
||||
export const tesseract: OcrTesseractProvider = {
|
||||
id: 'tesseract',
|
||||
name: 'Tesseract',
|
||||
capabilities: {
|
||||
image: true
|
||||
},
|
||||
config: {
|
||||
langs: {
|
||||
chi_sim: true,
|
||||
chi_tra: true,
|
||||
eng: true
|
||||
},
|
||||
enabled: false
|
||||
}
|
||||
} as const
|
||||
|
||||
export const systemOcr: OcrSystemProvider = {
|
||||
id: 'system',
|
||||
name: 'System',
|
||||
capabilities: {
|
||||
image: true
|
||||
// pdf: true
|
||||
},
|
||||
config: {
|
||||
langs: ['en-us'],
|
||||
enabled: false
|
||||
}
|
||||
} as const satisfies OcrSystemProvider
|
||||
|
||||
export const ppocrOcr: OcrPpocrProvider = {
|
||||
id: 'paddleocr',
|
||||
name: 'PaddleOCR',
|
||||
capabilities: {
|
||||
image: true
|
||||
// pdf: true
|
||||
},
|
||||
config: { apiUrl: '', enabled: false }
|
||||
} as const
|
||||
|
||||
export const ovOcr: OcrOvProvider = {
|
||||
id: 'ovocr',
|
||||
name: 'Intel OV(NPU) OCR',
|
||||
capabilities: {
|
||||
image: true
|
||||
// pdf: true
|
||||
},
|
||||
config: {
|
||||
enabled: false
|
||||
}
|
||||
} as const satisfies OcrOvProvider
|
||||
|
||||
export const INITIAL_BUILTIN_OCR_PROVIDER_MAP = {
|
||||
tesseract,
|
||||
system: systemOcr,
|
||||
paddleocr: ppocrOcr,
|
||||
ovocr: ovOcr
|
||||
} as const satisfies Record<BuiltinOcrProviderId, BuiltinOcrProvider>
|
||||
|
||||
export const BUILTIN_OCR_PROVIDERS: BuiltinOcrProvider[] = Object.values(INITIAL_BUILTIN_OCR_PROVIDER_MAP)
|
||||
|
||||
export const TESSERACT_LANG_MAP: Record<TranslateLanguageCode, TesseractLangCode> = {
|
||||
'af-za': 'afr',
|
||||
'am-et': 'amh',
|
||||
'ar-sa': 'ara',
|
||||
'as-in': 'asm',
|
||||
'az-az': 'aze',
|
||||
'az-cyrl-az': 'aze_cyrl',
|
||||
'be-by': 'bel',
|
||||
'bn-bd': 'ben',
|
||||
'bo-cn': 'bod',
|
||||
'bs-ba': 'bos',
|
||||
'bg-bg': 'bul',
|
||||
'ca-es': 'cat',
|
||||
'ceb-ph': 'ceb',
|
||||
'cs-cz': 'ces',
|
||||
'zh-cn': 'chi_sim',
|
||||
'zh-tw': 'chi_tra',
|
||||
'chr-us': 'chr',
|
||||
'cy-gb': 'cym',
|
||||
'da-dk': 'dan',
|
||||
'de-de': 'deu',
|
||||
'dz-bt': 'dzo',
|
||||
'el-gr': 'ell',
|
||||
'en-us': 'eng',
|
||||
'enm-gb': 'enm',
|
||||
'eo-world': 'epo',
|
||||
'et-ee': 'est',
|
||||
'eu-es': 'eus',
|
||||
'fa-ir': 'fas',
|
||||
'fi-fi': 'fin',
|
||||
'fr-fr': 'fra',
|
||||
'frk-de': 'frk',
|
||||
'frm-fr': 'frm',
|
||||
'ga-ie': 'gle',
|
||||
'gl-es': 'glg',
|
||||
'grc-gr': 'grc',
|
||||
'gu-in': 'guj',
|
||||
'ht-ht': 'hat',
|
||||
'he-il': 'heb',
|
||||
'hi-in': 'hin',
|
||||
'hr-hr': 'hrv',
|
||||
'hu-hu': 'hun',
|
||||
'iu-ca': 'iku',
|
||||
'id-id': 'ind',
|
||||
'is-is': 'isl',
|
||||
'it-it': 'ita',
|
||||
'ita-it': 'ita_old',
|
||||
'jv-id': 'jav',
|
||||
'ja-jp': 'jpn',
|
||||
'kn-in': 'kan',
|
||||
'ka-ge': 'kat',
|
||||
'kat-ge': 'kat_old',
|
||||
'kk-kz': 'kaz',
|
||||
'km-kh': 'khm',
|
||||
'ky-kg': 'kir',
|
||||
'ko-kr': 'kor',
|
||||
'ku-tr': 'kur',
|
||||
'la-la': 'lao',
|
||||
'la-va': 'lat',
|
||||
'lv-lv': 'lav',
|
||||
'lt-lt': 'lit',
|
||||
'ml-in': 'mal',
|
||||
'mr-in': 'mar',
|
||||
'mk-mk': 'mkd',
|
||||
'mt-mt': 'mlt',
|
||||
'ms-my': 'msa',
|
||||
'my-mm': 'mya',
|
||||
'ne-np': 'nep',
|
||||
'nl-nl': 'nld',
|
||||
'no-no': 'nor',
|
||||
'or-in': 'ori',
|
||||
'pa-in': 'pan',
|
||||
'pl-pl': 'pol',
|
||||
'pt-pt': 'por',
|
||||
'ps-af': 'pus',
|
||||
'ro-ro': 'ron',
|
||||
'ru-ru': 'rus',
|
||||
'sa-in': 'san',
|
||||
'si-lk': 'sin',
|
||||
'sk-sk': 'slk',
|
||||
'sl-si': 'slv',
|
||||
'es-es': 'spa',
|
||||
'spa-es': 'spa_old',
|
||||
'sq-al': 'sqi',
|
||||
'sr-rs': 'srp',
|
||||
'sr-latn-rs': 'srp_latn',
|
||||
'sw-tz': 'swa',
|
||||
'sv-se': 'swe',
|
||||
'syr-sy': 'syr',
|
||||
'ta-in': 'tam',
|
||||
'te-in': 'tel',
|
||||
'tg-tj': 'tgk',
|
||||
'tl-ph': 'tgl',
|
||||
'th-th': 'tha',
|
||||
'ti-er': 'tir',
|
||||
'tr-tr': 'tur',
|
||||
'ug-cn': 'uig',
|
||||
'uk-ua': 'ukr',
|
||||
'ur-pk': 'urd',
|
||||
'uz-uz': 'uzb',
|
||||
'uz-cyrl-uz': 'uzb_cyrl',
|
||||
'vi-vn': 'vie',
|
||||
'yi-us': 'yid'
|
||||
}
|
||||
@@ -1,18 +1,5 @@
|
||||
// NOTE: Types are defined inline in the schema for simplicity
|
||||
// If needed, specific types can be imported from './apiModels'
|
||||
import type {
|
||||
CreateOcrProviderRequest,
|
||||
CreateOcrProviderResponse,
|
||||
GetOcrProviderResponse,
|
||||
ListOcrProvidersQuery,
|
||||
ListOcrProvidersResponse,
|
||||
OcrProviderId,
|
||||
ReplaceOcrProviderRequest,
|
||||
ReplaceOcrProviderResponse,
|
||||
UpdateOcrProviderRequest,
|
||||
UpdateOcrProviderResponse
|
||||
} from '@types'
|
||||
|
||||
import type { BodyForPath, ConcreteApiPaths, QueryParamsForPath, ResponseForPath } from './apiPaths'
|
||||
import type { HttpMethod, PaginatedResponse, PaginationParams } from './apiTypes'
|
||||
|
||||
@@ -358,38 +345,6 @@ export interface ApiSchemas {
|
||||
}>
|
||||
}
|
||||
}
|
||||
|
||||
'/ocr/providers': {
|
||||
GET: {
|
||||
query: ListOcrProvidersQuery
|
||||
response: ListOcrProvidersResponse
|
||||
}
|
||||
POST: {
|
||||
body: CreateOcrProviderRequest
|
||||
response: CreateOcrProviderResponse
|
||||
}
|
||||
}
|
||||
|
||||
'/ocr/providers/:id': {
|
||||
GET: {
|
||||
params: { id: OcrProviderId }
|
||||
response: GetOcrProviderResponse
|
||||
}
|
||||
PATCH: {
|
||||
params: { id: OcrProviderId }
|
||||
body: UpdateOcrProviderRequest
|
||||
response: UpdateOcrProviderResponse
|
||||
}
|
||||
PUT: {
|
||||
params: { id: OcrProviderId }
|
||||
body: ReplaceOcrProviderRequest
|
||||
response: ReplaceOcrProviderResponse
|
||||
}
|
||||
DELETE: {
|
||||
params: { id: OcrProviderId }
|
||||
response: void
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -351,8 +351,6 @@ export interface PreferenceSchemas {
|
||||
'feature.translate.model_prompt': string
|
||||
// redux/settings/targetLanguage
|
||||
'feature.translate.target_language': string
|
||||
// redux/ocr/imageProviderId
|
||||
'ocr.settings.image_provider_id': string | null
|
||||
// redux/shortcuts/shortcuts.exit_fullscreen
|
||||
'shortcut.app.exit_fullscreen': Record<string, unknown>
|
||||
// redux/shortcuts/shortcuts.search_message
|
||||
@@ -401,6 +399,7 @@ export interface PreferenceSchemas {
|
||||
'ui.custom_css': string
|
||||
// redux/settings/navbarPosition
|
||||
'ui.navbar.position': 'left' | 'top'
|
||||
'ui.provider.show_disabled': boolean
|
||||
// redux/settings/sidebarIcons.disabled
|
||||
'ui.sidebar.icons.invisible': PreferenceTypes.SidebarIcon[]
|
||||
// redux/settings/sidebarIcons.visible
|
||||
@@ -614,7 +613,6 @@ export const DefaultPreferences: PreferenceSchemas = {
|
||||
'feature.selection.trigger_mode': PreferenceTypes.SelectionTriggerMode.Selected,
|
||||
'feature.translate.model_prompt': TRANSLATE_PROMPT,
|
||||
'feature.translate.target_language': 'en-us',
|
||||
'ocr.settings.image_provider_id': null,
|
||||
'shortcut.app.exit_fullscreen': { editable: false, enabled: true, key: ['Escape'], system: true },
|
||||
'shortcut.app.search_message': {
|
||||
editable: true,
|
||||
@@ -659,6 +657,7 @@ export const DefaultPreferences: PreferenceSchemas = {
|
||||
'topic.tab.show_time': false,
|
||||
'ui.custom_css': '',
|
||||
'ui.navbar.position': 'top',
|
||||
'ui.provider.show_disabled': true,
|
||||
'ui.sidebar.icons.invisible': [],
|
||||
'ui.sidebar.icons.visible': [
|
||||
'assistants',
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from './json'
|
||||
export * from './net'
|
||||
@@ -1,7 +0,0 @@
|
||||
export function safeParseJson(text: string): unknown | null {
|
||||
try {
|
||||
return JSON.parse(text)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -1,151 +1,152 @@
|
||||
# UI Component Library Migration Status
|
||||
# UI 组件库迁移状态
|
||||
|
||||
## Usage Example
|
||||
## 使用示例
|
||||
|
||||
```typescript
|
||||
// Import components from @cherrystudio/ui
|
||||
import { Spinner, DividerWithText, InfoTooltip } from '@cherrystudio/ui'
|
||||
// 从 @cherrystudio/ui 导入组件
|
||||
import { Spinner, DividerWithText, InfoTooltip, CustomTag } from '@cherrystudio/ui'
|
||||
|
||||
// Use in components
|
||||
// 在组件中使用
|
||||
function MyComponent() {
|
||||
return (
|
||||
<div>
|
||||
<Spinner size={24} />
|
||||
<DividerWithText text="Divider Text" />
|
||||
<InfoTooltip content="Tooltip message" />
|
||||
<DividerWithText text="分隔文本" />
|
||||
<InfoTooltip content="提示信息" />
|
||||
<CustomTag color="var(--color-primary)">标签</CustomTag>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Directory Structure
|
||||
## 目录结构说明
|
||||
|
||||
```text
|
||||
@packages/ui/
|
||||
├── src/
|
||||
│ ├── components/ # Main components directory
|
||||
│ │ ├── base/ # Basic components (buttons, inputs, labels, etc.)
|
||||
│ │ ├── display/ # Display components (cards, lists, tables, etc.)
|
||||
│ │ ├── layout/ # Layout components (containers, grids, spacing, etc.)
|
||||
│ │ ├── icons/ # Icon components
|
||||
│ │ ├── interactive/ # Interactive components (modals, tooltips, dropdowns, etc.)
|
||||
│ │ └── composite/ # Composite components (made from multiple base components)
|
||||
│ ├── hooks/ # Custom React Hooks
|
||||
│ └── types/ # TypeScript type definitions
|
||||
│ ├── components/ # 组件主目录
|
||||
│ │ ├── base/ # 基础组件(按钮、输入框、标签等)
|
||||
│ │ ├── display/ # 显示组件(卡片、列表、表格等)
|
||||
│ │ ├── layout/ # 布局组件(容器、网格、间距等)
|
||||
│ │ ├── icons/ # 图标组件
|
||||
│ │ ├── interactive/ # 交互组件(弹窗、提示、下拉等)
|
||||
│ │ └── composite/ # 复合组件(多个基础组件组合而成)
|
||||
│ ├── hooks/ # 自定义 React Hooks
|
||||
│ └── types/ # TypeScript 类型定义
|
||||
```
|
||||
|
||||
### Component Classification Guide
|
||||
### 组件分类指南
|
||||
|
||||
When submitting PRs, please place components in the correct directory based on their function:
|
||||
提交 PR 时,请根据组件功能将其放入正确的目录:
|
||||
|
||||
- **base**: Most basic UI elements like buttons, inputs, switches, labels, etc.
|
||||
- **display**: Components for displaying content like cards, lists, tables, tabs, etc.
|
||||
- **layout**: Components for page layout like containers, grid systems, dividers, etc.
|
||||
- **icons**: All icon-related components
|
||||
- **interactive**: Components requiring user interaction like modals, drawers, tooltips, dropdowns, etc.
|
||||
- **composite**: Composite components made from multiple base components
|
||||
- **base**: 最基础的 UI 元素,如按钮、输入框、开关、标签等
|
||||
- **display**: 用于展示内容的组件,如卡片、列表、表格、标签页等
|
||||
- **layout**: 用于页面布局的组件,如容器、网格系统、分隔符等
|
||||
- **icons**: 所有图标相关的组件
|
||||
- **interactive**: 需要用户交互的组件,如模态框、抽屉、提示框、下拉菜单等
|
||||
- **composite**: 复合组件,由多个基础组件组合而成
|
||||
|
||||
## Migration Overview
|
||||
## 迁移概览
|
||||
|
||||
- **Total Components**: 236
|
||||
- **Migrated**: 34
|
||||
- **Refactored**: 18
|
||||
- **Pending Migration**: 184
|
||||
- **总组件数**: 236
|
||||
- **已迁移**: 34
|
||||
- **已重构**: 18
|
||||
- **待迁移**: 184
|
||||
|
||||
## Component Status Table
|
||||
## 组件状态表
|
||||
|
||||
| Category | Component Name | Migration Status | Refactoring Status | Description |
|
||||
| ----------------- | ------------------------- | ---------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| **base** | | | | Base components |
|
||||
| | CopyButton | ✅ | ✅ | Copy button |
|
||||
| | CustomTag | ✅ | ✅ | Custom tag |
|
||||
| | DividerWithText | ✅ | ✅ | Divider with text |
|
||||
| | EmojiIcon | ✅ | ✅ | Emoji icon |
|
||||
| | ErrorBoundary | ✅ | ✅ | Error boundary (decoupled via props) |
|
||||
| | StatusTag | ✅ | ✅ | Unified status tag (merged ErrorTag, SuccessTag, WarnTag, InfoTag) |
|
||||
| | IndicatorLight | ✅ | ✅ | Indicator light |
|
||||
| | Spinner | ✅ | ✅ | Loading spinner |
|
||||
| | TextBadge | ✅ | ✅ | Text badge |
|
||||
| | CustomCollapse | ✅ | ✅ | Custom collapse panel |
|
||||
| **display** | | | | Display components |
|
||||
| | Ellipsis | ✅ | ✅ | Text ellipsis |
|
||||
| | ExpandableText | ✅ | ✅ | Expandable text |
|
||||
| | ThinkingEffect | ✅ | ✅ | Thinking effect animation |
|
||||
| | EmojiAvatar | ✅ | ✅ | Emoji avatar |
|
||||
| | ListItem | ✅ | ✅ | List item |
|
||||
| | MaxContextCount | ✅ | ✅ | Max context count display |
|
||||
| | ProviderAvatar | ✅ | ✅ | Provider avatar |
|
||||
| | CodeViewer | ❌ | ❌ | Code viewer (external deps) |
|
||||
| | OGCard | ❌ | ❌ | OG card |
|
||||
| | MarkdownShadowDOMRenderer | ❌ | ❌ | Markdown renderer |
|
||||
| | Preview/* | ❌ | ❌ | Preview components |
|
||||
| **layout** | | | | Layout components |
|
||||
| | HorizontalScrollContainer | ✅ | ❌ | Horizontal scroll container |
|
||||
| | Scrollbar | ✅ | ❌ | Scrollbar |
|
||||
| | Layout/* | ✅ | ✅ | Layout components |
|
||||
| | Tab/* | ❌ | ❌ | Tab (Redux dependency) |
|
||||
| | TopView | ❌ | ❌ | Top view (window.api dependency) |
|
||||
| **icons** | | | | Icon components |
|
||||
| | Icon | ✅ | ✅ | Icon factory function and predefined icons (merged CopyIcon, DeleteIcon, EditIcon, RefreshIcon, ResetIcon, ToolIcon, VisionIcon, WebSearchIcon, WrapIcon, UnWrapIcon, OcrIcon) |
|
||||
| | FileIcons | ✅ | ❌ | File icons (FileSvgIcon, FilePngIcon) |
|
||||
| | ReasoningIcon | ✅ | ❌ | Reasoning icon |
|
||||
| | SvgSpinners180Ring | ✅ | ❌ | Spinner loading icon |
|
||||
| | ToolsCallingIcon | ✅ | ❌ | Tools calling icon |
|
||||
| **interactive** | | | | Interactive components |
|
||||
| | InfoTooltip | ✅ | ❌ | Info tooltip |
|
||||
| | HelpTooltip | ✅ | ❌ | Help tooltip |
|
||||
| | WarnTooltip | ✅ | ❌ | Warning tooltip |
|
||||
| | EditableNumber | ✅ | ❌ | Editable number |
|
||||
| | InfoPopover | ✅ | ❌ | Info popover |
|
||||
| | CollapsibleSearchBar | ✅ | ❌ | Collapsible search bar |
|
||||
| | ImageToolButton | ✅ | ❌ | Image tool button |
|
||||
| | DraggableList | ✅ | ❌ | Draggable list |
|
||||
| | CodeEditor | ✅ | ❌ | Code editor |
|
||||
| | EmojiPicker | ❌ | ❌ | Emoji picker (useTheme dependency) |
|
||||
| | Selector | ✅ | ❌ | Selector (i18n dependency) |
|
||||
| | ModelSelector | ❌ | ❌ | Model selector (Redux dependency) |
|
||||
| | LanguageSelect | ❌ | ❌ | Language select |
|
||||
| | TranslateButton | ❌ | ❌ | Translate button (window.api dependency) |
|
||||
| **composite** | | | | Composite components |
|
||||
| | - | - | - | No composite components yet |
|
||||
| **Uncategorized** | | | | Components needing categorization |
|
||||
| | Popups/* (16+ files) | ❌ | ❌ | Popup components (business coupled) |
|
||||
| | RichEditor/* (30+ files) | ❌ | ❌ | Rich text editor |
|
||||
| | MarkdownEditor/* | ❌ | ❌ | Markdown editor |
|
||||
| | MinApp/* | ❌ | ❌ | Mini app (Redux dependency) |
|
||||
| | Avatar/* | ❌ | ❌ | Avatar components |
|
||||
| | ActionTools/* | ❌ | ❌ | Action tools |
|
||||
| | CodeBlockView/* | ❌ | ❌ | Code block view (window.api dependency) |
|
||||
| | ContextMenu | ❌ | ❌ | Context menu (Electron API) |
|
||||
| | WindowControls | ❌ | ❌ | Window controls (Electron API) |
|
||||
| | ErrorBoundary | ❌ | ❌ | Error boundary (window.api dependency) |
|
||||
| Category | Component Name | Migration Status | Refactoring Status | Description |
|
||||
| --------------- | ------------------------- | ---------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **base** | | | | 基础组件 |
|
||||
| | CopyButton | ✅ | ✅ | 复制按钮 |
|
||||
| | CustomTag | ✅ | ✅ | 自定义标签 |
|
||||
| | DividerWithText | ✅ | ✅ | 带文本的分隔线 |
|
||||
| | EmojiIcon | ✅ | ✅ | 表情图标 |
|
||||
| | ErrorBoundary | ✅ | ✅ | 错误边界 (通过 props 解耦) |
|
||||
| | StatusTag | ✅ | ✅ | 统一状态标签(合并了 ErrorTag、SuccessTag、WarnTag、InfoTag) |
|
||||
| | IndicatorLight | ✅ | ✅ | 指示灯 |
|
||||
| | Spinner | ✅ | ✅ | 加载动画 |
|
||||
| | TextBadge | ✅ | ✅ | 文本徽标 |
|
||||
| | CustomCollapse | ✅ | ✅ | 自定义折叠面板 |
|
||||
| **display** | | | | 显示组件 |
|
||||
| | Ellipsis | ✅ | ✅ | 文本省略 |
|
||||
| | ExpandableText | ✅ | ✅ | 可展开文本 |
|
||||
| | ThinkingEffect | ✅ | ✅ | 思考效果动画 |
|
||||
| | EmojiAvatar | ✅ | ✅ | 表情头像 |
|
||||
| | ListItem | ✅ | ✅ | 列表项 |
|
||||
| | MaxContextCount | ✅ | ✅ | 最大上下文数显示 |
|
||||
| | ProviderAvatar | ✅ | ✅ | 提供者头像 |
|
||||
| | CodeViewer | ❌ | ❌ | 代码查看器 (外部依赖) |
|
||||
| | OGCard | ❌ | ❌ | OG 卡片 |
|
||||
| | MarkdownShadowDOMRenderer | ❌ | ❌ | Markdown 渲染器 |
|
||||
| | Preview/* | ❌ | ❌ | 预览组件 |
|
||||
| **layout** | | | | 布局组件 |
|
||||
| | HorizontalScrollContainer | ✅ | ❌ | 水平滚动容器 |
|
||||
| | Scrollbar | ✅ | ❌ | 滚动条 |
|
||||
| | Layout/* | ✅ | ✅ | 布局组件 |
|
||||
| | Tab/* | ❌ | ❌ | 标签页 (Redux 依赖) |
|
||||
| | TopView | ❌ | ❌ | 顶部视图 (window.api 依赖) |
|
||||
| **icons** | | | | 图标组件 |
|
||||
| | Icon | ✅ | ✅ | 图标工厂函数和预定义图标(合并了 CopyIcon、DeleteIcon、EditIcon、RefreshIcon、ResetIcon、ToolIcon、VisionIcon、WebSearchIcon、WrapIcon、UnWrapIcon、OcrIcon) |
|
||||
| | FileIcons | ✅ | ❌ | 文件图标 (FileSvgIcon、FilePngIcon) |
|
||||
| | ReasoningIcon | ✅ | ❌ | 推理图标 |
|
||||
| | SvgSpinners180Ring | ✅ | ❌ | 旋转加载图标 |
|
||||
| | ToolsCallingIcon | ✅ | ❌ | 工具调用图标 |
|
||||
| **interactive** | | | | 交互组件 |
|
||||
| | InfoTooltip | ✅ | ❌ | 信息提示 |
|
||||
| | HelpTooltip | ✅ | ❌ | 帮助提示 |
|
||||
| | WarnTooltip | ✅ | ❌ | 警告提示 |
|
||||
| | EditableNumber | ✅ | ❌ | 可编辑数字 |
|
||||
| | InfoPopover | ✅ | ❌ | 信息弹出框 |
|
||||
| | CollapsibleSearchBar | ✅ | ❌ | 可折叠搜索栏 |
|
||||
| | ImageToolButton | ✅ | ❌ | 图片工具按钮 |
|
||||
| | DraggableList | ✅ | ❌ | 可拖拽列表 |
|
||||
| | CodeEditor | ✅ | ❌ | 代码编辑器 |
|
||||
| | EmojiPicker | ❌ | ❌ | 表情选择器 (useTheme 依赖) |
|
||||
| | Selector | ✅ | ❌ | 选择器 (i18n 依赖) |
|
||||
| | ModelSelector | ❌ | ❌ | 模型选择器 (Redux 依赖) |
|
||||
| | LanguageSelect | ❌ | ❌ | 语言选择 |
|
||||
| | TranslateButton | ❌ | ❌ | 翻译按钮 (window.api 依赖) |
|
||||
| **composite** | | | | 复合组件 |
|
||||
| | - | - | - | 暂无复合组件 |
|
||||
| **未分类** | | | | 需要分类的组件 |
|
||||
| | Popups/* (16+ 文件) | ❌ | ❌ | 弹窗组件 (业务耦合) |
|
||||
| | RichEditor/* (30+ 文件) | ❌ | ❌ | 富文本编辑器 |
|
||||
| | MarkdownEditor/* | ❌ | ❌ | Markdown 编辑器 |
|
||||
| | MinApp/* | ❌ | ❌ | 迷你应用 (Redux 依赖) |
|
||||
| | Avatar/* | ❌ | ❌ | 头像组件 |
|
||||
| | ActionTools/* | ❌ | ❌ | 操作工具 |
|
||||
| | CodeBlockView/* | ❌ | ❌ | 代码块视图 (window.api 依赖) |
|
||||
| | ContextMenu | ❌ | ❌ | 右键菜单 (Electron API) |
|
||||
| | WindowControls | ❌ | ❌ | 窗口控制 (Electron API) |
|
||||
| | ErrorBoundary | ❌ | ❌ | 错误边界 (window.api 依赖) |
|
||||
|
||||
## Migration Steps
|
||||
## 迁移步骤
|
||||
|
||||
### Phase 1: Copy Migration (Current Phase)
|
||||
### 第一阶段:复制迁移(当前阶段)
|
||||
|
||||
- Copy components as-is to @packages/ui
|
||||
- Retain original dependencies (antd, styled-components, etc.)
|
||||
- Add original path comment at file top
|
||||
- 将组件原样复制到 @packages/ui
|
||||
- 保留原有依赖(antd、styled-components 等)
|
||||
- 在文件顶部添加原路径注释
|
||||
|
||||
### Phase 2: Refactor and Optimize
|
||||
### 第二阶段:重构优化
|
||||
|
||||
- Remove antd dependencies, replace with HeroUI
|
||||
- Remove styled-components, replace with Tailwind CSS
|
||||
- Optimize component APIs and type definitions
|
||||
- 移除 antd 依赖,替换为 HeroUI
|
||||
- 移除 styled-components,替换为 Tailwind CSS
|
||||
- 优化组件 API 和类型定义
|
||||
|
||||
## 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
|
||||
1. **不迁移**包含以下依赖的组件(解耦后可迁移):
|
||||
- window.api 调用
|
||||
- Redux(useSelector、useDispatch 等)
|
||||
- 其他外部数据源
|
||||
|
||||
2. **Can migrate** but need decoupling later:
|
||||
- Components using i18n (change i18n to props)
|
||||
- Components using antd (replace with HeroUI later)
|
||||
2. **可迁移**但需要后续解耦的组件:
|
||||
- 使用 i18n 的组件(将 i18n 改为 props 传入)
|
||||
- 使用 antd 的组件(后续替换为 HeroUI)
|
||||
|
||||
3. **Submission Guidelines**:
|
||||
- Each PR should focus on one category of components
|
||||
- Ensure all migrated components are exported
|
||||
- Update migration status in this document
|
||||
3. **提交规范**:
|
||||
- 每次 PR 专注于一个类别的组件
|
||||
- 确保所有迁移的组件都有导出
|
||||
- 更新此文档的迁移状态
|
||||
|
||||
151
packages/ui/MIGRATION_STATUS_EN.md
Normal file
151
packages/ui/MIGRATION_STATUS_EN.md
Normal file
@@ -0,0 +1,151 @@
|
||||
# UI Component Library Migration Status
|
||||
|
||||
## 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
|
||||
│ │ ├── base/ # Basic components (buttons, inputs, labels, etc.)
|
||||
│ │ ├── display/ # Display components (cards, lists, tables, etc.)
|
||||
│ │ ├── layout/ # Layout components (containers, grids, spacing, etc.)
|
||||
│ │ ├── icons/ # Icon components
|
||||
│ │ ├── interactive/ # Interactive components (modals, tooltips, dropdowns, etc.)
|
||||
│ │ └── composite/ # Composite components (made from multiple base components)
|
||||
│ ├── hooks/ # Custom React Hooks
|
||||
│ └── types/ # TypeScript type definitions
|
||||
```
|
||||
|
||||
### Component Classification Guide
|
||||
|
||||
When submitting PRs, please place components in the correct directory based on their function:
|
||||
|
||||
- **base**: Most basic UI elements like buttons, inputs, switches, labels, etc.
|
||||
- **display**: Components for displaying content like cards, lists, tables, tabs, etc.
|
||||
- **layout**: Components for page layout like containers, grid systems, dividers, etc.
|
||||
- **icons**: All icon-related components
|
||||
- **interactive**: Components requiring user interaction like modals, drawers, tooltips, dropdowns, etc.
|
||||
- **composite**: Composite components made from multiple base components
|
||||
|
||||
## Migration Overview
|
||||
|
||||
- **Total Components**: 236
|
||||
- **Migrated**: 34
|
||||
- **Refactored**: 18
|
||||
- **Pending Migration**: 184
|
||||
|
||||
## Component Status Table
|
||||
|
||||
| Category | Component Name | Migration Status | Refactoring Status | Description |
|
||||
| ----------------- | ------------------------- | ---------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| **base** | | | | Base components |
|
||||
| | CopyButton | ✅ | ✅ | Copy button |
|
||||
| | CustomTag | ✅ | ✅ | Custom tag |
|
||||
| | DividerWithText | ✅ | ✅ | Divider with text |
|
||||
| | EmojiIcon | ✅ | ✅ | Emoji icon |
|
||||
| | ErrorBoundary | ✅ | ✅ | Error boundary (decoupled via props) |
|
||||
| | StatusTag | ✅ | ✅ | Unified status tag (merged ErrorTag, SuccessTag, WarnTag, InfoTag) |
|
||||
| | IndicatorLight | ✅ | ✅ | Indicator light |
|
||||
| | Spinner | ✅ | ✅ | Loading spinner |
|
||||
| | TextBadge | ✅ | ✅ | Text badge |
|
||||
| | CustomCollapse | ✅ | ✅ | Custom collapse panel |
|
||||
| **display** | | | | Display components |
|
||||
| | Ellipsis | ✅ | ✅ | Text ellipsis |
|
||||
| | ExpandableText | ✅ | ✅ | Expandable text |
|
||||
| | ThinkingEffect | ✅ | ✅ | Thinking effect animation |
|
||||
| | EmojiAvatar | ✅ | ✅ | Emoji avatar |
|
||||
| | ListItem | ✅ | ✅ | List item |
|
||||
| | MaxContextCount | ✅ | ✅ | Max context count display |
|
||||
| | ProviderAvatar | ✅ | ✅ | Provider avatar |
|
||||
| | CodeViewer | ❌ | ❌ | Code viewer (external deps) |
|
||||
| | OGCard | ❌ | ❌ | OG card |
|
||||
| | MarkdownShadowDOMRenderer | ❌ | ❌ | Markdown renderer |
|
||||
| | Preview/* | ❌ | ❌ | Preview components |
|
||||
| **layout** | | | | Layout components |
|
||||
| | HorizontalScrollContainer | ✅ | ❌ | Horizontal scroll container |
|
||||
| | Scrollbar | ✅ | ❌ | Scrollbar |
|
||||
| | Layout/* | ✅ | ✅ | Layout components |
|
||||
| | Tab/* | ❌ | ❌ | Tab (Redux dependency) |
|
||||
| | TopView | ❌ | ❌ | Top view (window.api dependency) |
|
||||
| **icons** | | | | Icon components |
|
||||
| | Icon | ✅ | ✅ | Icon factory function and predefined icons (merged CopyIcon, DeleteIcon, EditIcon, RefreshIcon, ResetIcon, ToolIcon, VisionIcon, WebSearchIcon, WrapIcon, UnWrapIcon, OcrIcon) |
|
||||
| | FileIcons | ✅ | ❌ | File icons (FileSvgIcon, FilePngIcon) |
|
||||
| | ReasoningIcon | ✅ | ❌ | Reasoning icon |
|
||||
| | SvgSpinners180Ring | ✅ | ❌ | Spinner loading icon |
|
||||
| | ToolsCallingIcon | ✅ | ❌ | Tools calling icon |
|
||||
| **interactive** | | | | Interactive components |
|
||||
| | InfoTooltip | ✅ | ❌ | Info tooltip |
|
||||
| | HelpTooltip | ✅ | ❌ | Help tooltip |
|
||||
| | WarnTooltip | ✅ | ❌ | Warning tooltip |
|
||||
| | EditableNumber | ✅ | ❌ | Editable number |
|
||||
| | InfoPopover | ✅ | ❌ | Info popover |
|
||||
| | CollapsibleSearchBar | ✅ | ❌ | Collapsible search bar |
|
||||
| | ImageToolButton | ✅ | ❌ | Image tool button |
|
||||
| | DraggableList | ✅ | ❌ | Draggable list |
|
||||
| | CodeEditor | ✅ | ❌ | Code editor |
|
||||
| | EmojiPicker | ❌ | ❌ | Emoji picker (useTheme dependency) |
|
||||
| | Selector | ✅ | ❌ | Selector (i18n dependency) |
|
||||
| | ModelSelector | ❌ | ❌ | Model selector (Redux dependency) |
|
||||
| | LanguageSelect | ❌ | ❌ | Language select |
|
||||
| | TranslateButton | ❌ | ❌ | Translate button (window.api dependency) |
|
||||
| **composite** | | | | Composite components |
|
||||
| | - | - | - | No composite components yet |
|
||||
| **Uncategorized** | | | | Components needing categorization |
|
||||
| | Popups/* (16+ files) | ❌ | ❌ | Popup components (business coupled) |
|
||||
| | RichEditor/* (30+ files) | ❌ | ❌ | Rich text editor |
|
||||
| | MarkdownEditor/* | ❌ | ❌ | Markdown editor |
|
||||
| | MinApp/* | ❌ | ❌ | Mini app (Redux dependency) |
|
||||
| | Avatar/* | ❌ | ❌ | Avatar components |
|
||||
| | ActionTools/* | ❌ | ❌ | Action tools |
|
||||
| | CodeBlockView/* | ❌ | ❌ | Code block view (window.api dependency) |
|
||||
| | ContextMenu | ❌ | ❌ | Context menu (Electron API) |
|
||||
| | WindowControls | ❌ | ❌ | Window controls (Electron API) |
|
||||
| | ErrorBoundary | ❌ | ❌ | Error boundary (window.api dependency) |
|
||||
|
||||
## Migration Steps
|
||||
|
||||
### Phase 1: Copy Migration (Current Phase)
|
||||
|
||||
- Copy components as-is to @packages/ui
|
||||
- Retain original dependencies (antd, styled-components, etc.)
|
||||
- Add original path comment at file top
|
||||
|
||||
### Phase 2: Refactor and Optimize
|
||||
|
||||
- Remove antd dependencies, replace with HeroUI
|
||||
- Remove styled-components, replace with Tailwind CSS
|
||||
- Optimize component APIs and type definitions
|
||||
|
||||
## 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 HeroUI later)
|
||||
|
||||
3. **Submission Guidelines**:
|
||||
- Each PR should focus on one category of components
|
||||
- Ensure all migrated components are exported
|
||||
- Update migration status in this document
|
||||
@@ -4,7 +4,7 @@
|
||||
"components": "@cherrystudio/ui/components",
|
||||
"hooks": "@cherrystudio/ui/hooks",
|
||||
"lib": "@cherrystudio/ui/lib",
|
||||
"ui": "@cherrystudio/ui/components/primitives",
|
||||
"ui": "@cherrystudio/ui/components/ui",
|
||||
"utils": "@cherrystudio/ui/utils"
|
||||
},
|
||||
"iconLibrary": "lucide",
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { cn } from '@heroui/react'
|
||||
import React, { memo } from 'react'
|
||||
|
||||
import { cn } from '../../../utils'
|
||||
|
||||
interface EmojiAvatarProps {
|
||||
children: string
|
||||
size?: number
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { AvatarProps as HeroUIAvatarProps } from '@heroui/react'
|
||||
import { Avatar as HeroUIAvatar, AvatarGroup as HeroUIAvatarGroup } from '@heroui/react'
|
||||
import { Avatar as HeroUIAvatar, AvatarGroup as HeroUIAvatarGroup, cn } from '@heroui/react'
|
||||
|
||||
import { cn } from '../../../utils'
|
||||
import EmojiAvatar from './EmojiAvatar'
|
||||
|
||||
export interface AvatarProps extends Omit<HeroUIAvatarProps, 'size'> {
|
||||
12
packages/ui/src/components/base/Button/index.tsx
Normal file
12
packages/ui/src/components/base/Button/index.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import type { ButtonProps as HeroUIButtonProps } from '@heroui/react'
|
||||
import { Button as HeroUIButton } from '@heroui/react'
|
||||
|
||||
export interface ButtonProps extends HeroUIButtonProps {}
|
||||
|
||||
const Button = ({ ...props }: ButtonProps) => {
|
||||
return <HeroUIButton {...props} />
|
||||
}
|
||||
|
||||
Button.displayName = 'Button'
|
||||
|
||||
export default Button
|
||||
@@ -1,9 +1,8 @@
|
||||
// Original path: src/renderer/src/components/CopyButton.tsx
|
||||
import { Tooltip } from '@heroui/react'
|
||||
import { Copy } from 'lucide-react'
|
||||
import type { FC } from 'react'
|
||||
|
||||
import { Tooltip } from './tooltip'
|
||||
|
||||
interface CopyButtonProps {
|
||||
tooltip?: string
|
||||
label?: string
|
||||
46
packages/ui/src/components/base/CustomCollapse/index.tsx
Normal file
46
packages/ui/src/components/base/CustomCollapse/index.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import { Accordion, AccordionItem, type AccordionItemProps, type AccordionProps } from '@heroui/react'
|
||||
import type { FC } from 'react'
|
||||
import { memo } from 'react'
|
||||
|
||||
// 重新导出 HeroUI 的组件,方便直接使用
|
||||
export { Accordion, AccordionItem } from '@heroui/react'
|
||||
|
||||
interface CustomCollapseProps {
|
||||
children: React.ReactNode
|
||||
accordionProps?: Omit<AccordionProps, 'children'>
|
||||
accordionItemProps?: Omit<AccordionItemProps, 'children'>
|
||||
}
|
||||
|
||||
const CustomCollapse: FC<CustomCollapseProps> = ({ children, accordionProps = {}, accordionItemProps = {} }) => {
|
||||
// 解构 Accordion 的 props
|
||||
const {
|
||||
defaultExpandedKeys = ['1'],
|
||||
variant = 'bordered',
|
||||
className = '',
|
||||
isDisabled = false,
|
||||
...restAccordionProps
|
||||
} = accordionProps
|
||||
|
||||
// 解构 AccordionItem 的 props
|
||||
const { title = 'Collapse Panel', ...restAccordionItemProps } = accordionItemProps
|
||||
|
||||
return (
|
||||
<Accordion
|
||||
defaultExpandedKeys={defaultExpandedKeys}
|
||||
variant={variant}
|
||||
className={className}
|
||||
isDisabled={isDisabled}
|
||||
selectionMode="multiple"
|
||||
{...restAccordionProps}>
|
||||
<AccordionItem
|
||||
key="1"
|
||||
aria-label={typeof title === 'string' ? title : 'collapse-item'}
|
||||
title={title}
|
||||
{...restAccordionItemProps}>
|
||||
{children}
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(CustomCollapse)
|
||||
@@ -1,10 +1,9 @@
|
||||
// Original path: src/renderer/src/components/Tags/CustomTag.tsx
|
||||
import { Tooltip } from '@heroui/react'
|
||||
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
|
||||
@@ -1,10 +1,10 @@
|
||||
// Original path: src/renderer/src/components/ErrorBoundary.tsx
|
||||
import { Button } from '@heroui/react'
|
||||
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 {
|
||||
@@ -35,12 +35,12 @@ const DefaultFallback: ComponentType<CustomFallbackProps> = (props: CustomFallba
|
||||
<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}>
|
||||
<Button size="sm" variant="flat" color="danger" onPress={onDebugClick}>
|
||||
{debugButtonText}
|
||||
</Button>
|
||||
)}
|
||||
{onReloadClick && (
|
||||
<Button size="sm" variant="destructive" onClick={onReloadClick}>
|
||||
<Button size="sm" variant="flat" color="danger" onPress={onReloadClick}>
|
||||
{reloadButtonText}
|
||||
</Button>
|
||||
)}
|
||||
333
packages/ui/src/components/base/Selector/README.md
Normal file
333
packages/ui/src/components/base/Selector/README.md
Normal file
@@ -0,0 +1,333 @@
|
||||
# Selector 组件
|
||||
|
||||
基于 HeroUI Select 封装的下拉选择组件,简化了 Set 和 Selection 的转换逻辑。
|
||||
|
||||
## 核心特性
|
||||
|
||||
- ✅ **类型安全**: 单选和多选自动推断回调类型
|
||||
- ✅ **智能转换**: 自动处理 `Set<Key>` 和原始值的转换
|
||||
- ✅ **HeroUI 风格**: 保持与 HeroUI 生态一致的 API
|
||||
- ✅ **支持数字和字符串**: 泛型支持,自动识别值类型
|
||||
|
||||
## 基础用法
|
||||
|
||||
### 单选模式(默认)
|
||||
|
||||
```tsx
|
||||
import { Selector } from '@cherrystudio/ui'
|
||||
import { useState } from 'react'
|
||||
|
||||
function Example() {
|
||||
const [language, setLanguage] = useState('zh-CN')
|
||||
|
||||
const languageOptions = [
|
||||
{ label: '中文', value: 'zh-CN' },
|
||||
{ label: 'English', value: 'en-US' },
|
||||
{ label: '日本語', value: 'ja-JP' }
|
||||
]
|
||||
|
||||
return (
|
||||
<Selector
|
||||
selectedKeys={language}
|
||||
onSelectionChange={(value) => {
|
||||
// value 类型自动推断为 string
|
||||
setLanguage(value)
|
||||
}}
|
||||
items={languageOptions}
|
||||
placeholder="选择语言"
|
||||
/>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 多选模式
|
||||
|
||||
```tsx
|
||||
import { Selector } from '@cherrystudio/ui'
|
||||
import { useState } from 'react'
|
||||
|
||||
function Example() {
|
||||
const [languages, setLanguages] = useState(['zh-CN', 'en-US'])
|
||||
|
||||
const languageOptions = [
|
||||
{ label: '中文', value: 'zh-CN' },
|
||||
{ label: 'English', value: 'en-US' },
|
||||
{ label: '日本語', value: 'ja-JP' },
|
||||
{ label: 'Français', value: 'fr-FR' }
|
||||
]
|
||||
|
||||
return (
|
||||
<Selector
|
||||
selectionMode="multiple"
|
||||
selectedKeys={languages}
|
||||
onSelectionChange={(values) => {
|
||||
// values 类型自动推断为 string[]
|
||||
setLanguages(values)
|
||||
}}
|
||||
items={languageOptions}
|
||||
placeholder="选择语言"
|
||||
/>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 数字类型值
|
||||
|
||||
```tsx
|
||||
import { Selector } from '@cherrystudio/ui'
|
||||
|
||||
function Example() {
|
||||
const [priority, setPriority] = useState<number>(1)
|
||||
|
||||
const priorityOptions = [
|
||||
{ label: '低', value: 1 },
|
||||
{ label: '中', value: 2 },
|
||||
{ label: '高', value: 3 }
|
||||
]
|
||||
|
||||
return (
|
||||
<Selector<number>
|
||||
selectedKeys={priority}
|
||||
onSelectionChange={(value) => {
|
||||
// value 类型为 number
|
||||
setPriority(value)
|
||||
}}
|
||||
items={priorityOptions}
|
||||
/>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 禁用选项
|
||||
|
||||
```tsx
|
||||
const options = [
|
||||
{ label: '选项 1', value: '1' },
|
||||
{ label: '选项 2 (禁用)', value: '2', disabled: true },
|
||||
{ label: '选项 3', value: '3' }
|
||||
]
|
||||
|
||||
<Selector
|
||||
selectedKeys="1"
|
||||
onSelectionChange={handleChange}
|
||||
items={options}
|
||||
/>
|
||||
```
|
||||
|
||||
### 自定义 Label
|
||||
|
||||
```tsx
|
||||
import { Flex } from '@cherrystudio/ui'
|
||||
|
||||
const options = [
|
||||
{
|
||||
label: (
|
||||
<Flex className="items-center gap-2">
|
||||
<span>🇨🇳</span>
|
||||
<span>中文</span>
|
||||
</Flex>
|
||||
),
|
||||
value: 'zh-CN'
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<Flex className="items-center gap-2">
|
||||
<span>🇺🇸</span>
|
||||
<span>English</span>
|
||||
</Flex>
|
||||
),
|
||||
value: 'en-US'
|
||||
}
|
||||
]
|
||||
|
||||
<Selector
|
||||
selectedKeys="zh-CN"
|
||||
onSelectionChange={handleChange}
|
||||
items={options}
|
||||
/>
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
### SelectorProps
|
||||
|
||||
| 属性 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| `items` | `SelectorItem<V>[]` | - | 必填,选项列表 |
|
||||
| `selectedKeys` | `V` \| `V[]` | - | 受控的选中值(单选为单个值,多选为数组) |
|
||||
| `onSelectionChange` | `(key: V) => void` \| `(keys: V[]) => void` | - | 选择变化回调(类型根据 selectionMode 自动推断) |
|
||||
| `selectionMode` | `'single'` \| `'multiple'` | `'single'` | 选择模式 |
|
||||
| `placeholder` | `string` | - | 占位文本 |
|
||||
| `disabled` | `boolean` | `false` | 是否禁用 |
|
||||
| `isRequired` | `boolean` | `false` | 是否必填 |
|
||||
| `label` | `ReactNode` | - | 标签文本 |
|
||||
| `description` | `ReactNode` | - | 描述文本 |
|
||||
| `errorMessage` | `ReactNode` | - | 错误提示 |
|
||||
| ...rest | `SelectProps` | - | 其他 HeroUI Select 属性 |
|
||||
|
||||
### SelectorItem
|
||||
|
||||
```tsx
|
||||
interface SelectorItem<V = string | number> {
|
||||
label: string | ReactNode // 显示文本或自定义内容
|
||||
value: V // 选项值
|
||||
disabled?: boolean // 是否禁用
|
||||
[key: string]: any // 其他自定义属性
|
||||
}
|
||||
```
|
||||
|
||||
## 类型安全
|
||||
|
||||
组件使用 TypeScript 条件类型,根据 `selectionMode` 自动推断回调类型:
|
||||
|
||||
```tsx
|
||||
// 单选模式
|
||||
<Selector
|
||||
selectionMode="single" // 或省略(默认单选)
|
||||
selectedKeys={value} // 类型: V
|
||||
onSelectionChange={(v) => ...} // v 类型: V
|
||||
/>
|
||||
|
||||
// 多选模式
|
||||
<Selector
|
||||
selectionMode="multiple"
|
||||
selectedKeys={values} // 类型: V[]
|
||||
onSelectionChange={(vs) => ...} // vs 类型: V[]
|
||||
/>
|
||||
```
|
||||
|
||||
## 与 HeroUI Select 的区别
|
||||
|
||||
| 特性 | HeroUI Select | Selector (本组件) |
|
||||
|------|---------------|------------------|
|
||||
| `selectedKeys` | `Set<Key> \| 'all'` | `V` \| `V[]` (自动转换) |
|
||||
| `onSelectionChange` | `(keys: Selection) => void` | `(key: V) => void` \| `(keys: V[]) => void` |
|
||||
| 单选回调 | 返回 `Set` (需手动提取) | 直接返回单个值 |
|
||||
| 多选回调 | 返回 `Set` (需转数组) | 直接返回数组 |
|
||||
| 类型推断 | 无 | 根据 selectionMode 自动推断 |
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 显式声明 selectionMode
|
||||
|
||||
虽然单选是默认模式,但建议显式声明以提高代码可读性:
|
||||
|
||||
```tsx
|
||||
// ✅ 推荐
|
||||
<Selector selectionMode="single" ... />
|
||||
|
||||
// ⚠️ 可以但不够清晰
|
||||
<Selector ... />
|
||||
```
|
||||
|
||||
### 2. 使用泛型指定值类型
|
||||
|
||||
当值类型为数字或联合类型时,使用泛型获得更好的类型提示:
|
||||
|
||||
```tsx
|
||||
// ✅ 推荐
|
||||
<Selector<number> selectedKeys={priority} ... />
|
||||
|
||||
// ✅ 推荐(联合类型)
|
||||
type Status = 'pending' | 'approved' | 'rejected'
|
||||
<Selector<Status> selectedKeys={status} ... />
|
||||
```
|
||||
|
||||
### 3. 避免在渲染时创建 items
|
||||
|
||||
```tsx
|
||||
// ❌ 不推荐(每次渲染都创建新数组)
|
||||
<Selector items={[{ label: 'A', value: '1' }]} />
|
||||
|
||||
// ✅ 推荐(在组件外或使用 useMemo)
|
||||
const items = [{ label: 'A', value: '1' }]
|
||||
<Selector items={items} />
|
||||
```
|
||||
|
||||
## 迁移指南
|
||||
|
||||
### 从 antd Select 迁移
|
||||
|
||||
```tsx
|
||||
// antd Select
|
||||
import { Select } from 'antd'
|
||||
|
||||
<Select
|
||||
value={value}
|
||||
onChange={(value) => onChange(value)}
|
||||
options={[
|
||||
{ label: 'A', value: '1' },
|
||||
{ label: 'B', value: '2' }
|
||||
]}
|
||||
/>
|
||||
|
||||
// 迁移到 Selector
|
||||
import { Selector } from '@cherrystudio/ui'
|
||||
|
||||
<Selector
|
||||
selectedKeys={value} // value → selectedKeys
|
||||
onSelectionChange={(value) => onChange(value)} // onChange → onSelectionChange
|
||||
items={[ // options → items
|
||||
{ label: 'A', value: '1' },
|
||||
{ label: 'B', value: '2' }
|
||||
]}
|
||||
/>
|
||||
```
|
||||
|
||||
### 从旧版 Selector 迁移
|
||||
|
||||
```tsx
|
||||
// 旧版 Selector (返回数组)
|
||||
<Selector
|
||||
onSelectionChange={(values) => {
|
||||
const value = values[0] // 需要手动提取
|
||||
onChange(value)
|
||||
}}
|
||||
/>
|
||||
|
||||
// 新版 Selector (直接返回值)
|
||||
<Selector
|
||||
selectionMode="single"
|
||||
onSelectionChange={(value) => {
|
||||
onChange(value) // 直接使用
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 为什么单选模式下还需要 selectedKeys 而不是 selectedKey?
|
||||
|
||||
A: 为了保持与 HeroUI API 命名的一致性,同时简化组件实现。组件内部会自动处理单个值和 Set 的转换。
|
||||
|
||||
### Q: 如何清空选择?
|
||||
|
||||
```tsx
|
||||
// 单选模式
|
||||
<Selector
|
||||
selectedKeys={value}
|
||||
onSelectionChange={setValue}
|
||||
isClearable // 添加清空按钮
|
||||
/>
|
||||
|
||||
// 或手动设置为 undefined
|
||||
setValue(undefined)
|
||||
```
|
||||
|
||||
### Q: 支持异步加载选项吗?
|
||||
|
||||
支持,配合 `isLoading` 属性使用:
|
||||
|
||||
```tsx
|
||||
const [items, setItems] = useState([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
fetchItems().then(data => {
|
||||
setItems(data)
|
||||
setLoading(false)
|
||||
})
|
||||
}, [])
|
||||
|
||||
<Selector items={items} isLoading={loading} />
|
||||
```
|
||||
53
packages/ui/src/components/base/StatusTag/index.tsx
Normal file
53
packages/ui/src/components/base/StatusTag/index.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import type { LucideIcon } from 'lucide-react'
|
||||
import { AlertTriangleIcon, CheckIcon, CircleXIcon, InfoIcon } from 'lucide-react'
|
||||
import React from 'react'
|
||||
|
||||
import CustomTag from '../CustomTag'
|
||||
|
||||
export type StatusType = 'success' | 'error' | 'warning' | 'info'
|
||||
|
||||
export interface StatusTagProps {
|
||||
type: StatusType
|
||||
message: string
|
||||
iconSize?: number
|
||||
icon?: React.ReactNode
|
||||
color?: string
|
||||
className?: string
|
||||
}
|
||||
|
||||
const statusConfig: Record<StatusType, { Icon: LucideIcon; color: string }> = {
|
||||
success: { Icon: CheckIcon, color: '#10B981' }, // green-500
|
||||
error: { Icon: CircleXIcon, color: '#EF4444' }, // red-500
|
||||
warning: { Icon: AlertTriangleIcon, color: '#F59E0B' }, // amber-500
|
||||
info: { Icon: InfoIcon, color: '#3B82F6' } // blue-500
|
||||
}
|
||||
|
||||
export const StatusTag: React.FC<StatusTagProps> = ({ type, message, iconSize = 14, icon, color, className }) => {
|
||||
const config = statusConfig[type]
|
||||
const Icon = config.Icon
|
||||
const finalColor = color || config.color
|
||||
const finalIcon = icon || <Icon size={iconSize} color={finalColor} />
|
||||
|
||||
return (
|
||||
<CustomTag icon={finalIcon} color={finalColor} className={className}>
|
||||
{message}
|
||||
</CustomTag>
|
||||
)
|
||||
}
|
||||
|
||||
// 保留原有的导出以保持向后兼容
|
||||
export const SuccessTag = ({ iconSize, message }: { iconSize?: number; message: string }) => (
|
||||
<StatusTag type="success" iconSize={iconSize} message={message} />
|
||||
)
|
||||
|
||||
export const ErrorTag = ({ iconSize, message }: { iconSize?: number; message: string }) => (
|
||||
<StatusTag type="error" iconSize={iconSize} message={message} />
|
||||
)
|
||||
|
||||
export const WarnTag = ({ iconSize, message }: { iconSize?: number; message: string }) => (
|
||||
<StatusTag type="warning" iconSize={iconSize} message={message} />
|
||||
)
|
||||
|
||||
export const InfoTag = ({ iconSize, message }: { iconSize?: number; message: string }) => (
|
||||
<StatusTag type="info" iconSize={iconSize} message={message} />
|
||||
)
|
||||
20
packages/ui/src/components/base/TextBadge/index.tsx
Normal file
20
packages/ui/src/components/base/TextBadge/index.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
// Original: src/renderer/src/components/TextBadge.tsx
|
||||
import type { FC } from 'react'
|
||||
|
||||
interface TextBadgeProps {
|
||||
text: string
|
||||
style?: React.CSSProperties
|
||||
className?: string
|
||||
}
|
||||
|
||||
const TextBadge: FC<TextBadgeProps> = ({ text, style, className = '' }) => {
|
||||
return (
|
||||
<span
|
||||
className={`text-xs text-blue-600 dark:text-blue-400 bg-blue-100 dark:bg-blue-900/30 px-1.5 py-0.5 rounded font-medium ${className}`}
|
||||
style={style}>
|
||||
{text}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export default TextBadge
|
||||
@@ -1,26 +1,33 @@
|
||||
// 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'
|
||||
// Base Components
|
||||
export { Avatar, AvatarGroup, type AvatarProps, EmojiAvatar } from './base/Avatar'
|
||||
export { default as Button, type ButtonProps } from './base/Button'
|
||||
export { default as CopyButton } from './base/CopyButton'
|
||||
export { default as CustomCollapse } from './base/CustomCollapse'
|
||||
export { default as CustomTag } from './base/CustomTag'
|
||||
export { default as DividerWithText } from './base/DividerWithText'
|
||||
export { default as EmojiIcon } from './base/EmojiIcon'
|
||||
export type { CustomFallbackProps, ErrorBoundaryCustomizedProps } from './base/ErrorBoundary'
|
||||
export { ErrorBoundary } from './base/ErrorBoundary'
|
||||
export { default as IndicatorLight } from './base/IndicatorLight'
|
||||
export { default as Spinner } from './base/Spinner'
|
||||
export type { StatusTagProps, StatusType } from './base/StatusTag'
|
||||
export { ErrorTag, InfoTag, StatusTag, SuccessTag, WarnTag } from './base/StatusTag'
|
||||
export { DescriptionSwitch, Switch } from './base/Switch'
|
||||
export { default as TextBadge } from './base/TextBadge'
|
||||
export { getToastUtilities, type ToastUtilities } from './base/Toast'
|
||||
export { Tooltip, type TooltipProps } from './base/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'
|
||||
// Display Components
|
||||
export { default as Ellipsis } from './display/Ellipsis'
|
||||
export { default as ExpandableText } from './display/ExpandableText'
|
||||
export { default as ListItem } from './display/ListItem'
|
||||
export { default as MaxContextCount } from './display/MaxContextCount'
|
||||
export { default as ThinkingEffect } from './display/ThinkingEffect'
|
||||
|
||||
// Layout Components
|
||||
export { Box, Center, ColFlex, Flex, RowFlex, SpaceBetweenRowFlex } from './layout/Flex'
|
||||
export { default as HorizontalScrollContainer } from './layout/HorizontalScrollContainer'
|
||||
export { default as Scrollbar } from './layout/Scrollbar'
|
||||
|
||||
// Icon Components
|
||||
export { FilePngIcon, FileSvgIcon } from './icons/FileIcons'
|
||||
@@ -42,9 +49,11 @@ export {
|
||||
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'
|
||||
/* Interactive Components */
|
||||
|
||||
// Selector / SearchableSelector
|
||||
export { default as Selector } from './base/Selector'
|
||||
export { default as SearchableSelector } from './base/Selector/SearchableSelector'
|
||||
export type {
|
||||
MultipleSearchableSelectorProps,
|
||||
MultipleSelectorProps,
|
||||
@@ -54,9 +63,7 @@ export type {
|
||||
SelectorProps,
|
||||
SingleSearchableSelectorProps,
|
||||
SingleSelectorProps
|
||||
} from './primitives/Selector/types'
|
||||
|
||||
/* Additional Composite Components */
|
||||
} from './base/Selector/types'
|
||||
// CodeEditor
|
||||
export {
|
||||
default as CodeEditor,
|
||||
@@ -65,27 +72,28 @@ export {
|
||||
type CodeMirrorTheme,
|
||||
getCmThemeByName,
|
||||
getCmThemeNames
|
||||
} from './composites/CodeEditor'
|
||||
} from './interactive/CodeEditor'
|
||||
// CollapsibleSearchBar
|
||||
export { default as CollapsibleSearchBar } from './composites/CollapsibleSearchBar'
|
||||
export { default as CollapsibleSearchBar } from './interactive/CollapsibleSearchBar'
|
||||
// DraggableList
|
||||
export { DraggableList, useDraggableReorder } from './composites/DraggableList'
|
||||
export { DraggableList, useDraggableReorder } from './interactive/DraggableList'
|
||||
// EditableNumber
|
||||
export type { EditableNumberProps } from './composites/EditableNumber'
|
||||
export { default as EditableNumber } from './composites/EditableNumber'
|
||||
export type { EditableNumberProps } from './interactive/EditableNumber'
|
||||
// EditableNumber
|
||||
export { default as EditableNumber } from './interactive/EditableNumber'
|
||||
// Tooltip variants
|
||||
export { HelpTooltip, type IconTooltipProps, InfoTooltip, WarnTooltip } from './composites/IconTooltips'
|
||||
export { HelpTooltip, type IconTooltipProps, InfoTooltip, WarnTooltip } from './interactive/IconTooltips'
|
||||
// ImageToolButton
|
||||
export { default as ImageToolButton } from './composites/ImageToolButton'
|
||||
export { default as ImageToolButton } from './interactive/ImageToolButton'
|
||||
// Sortable
|
||||
export { Sortable } from './composites/Sortable'
|
||||
export { Sortable } from './interactive/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'
|
||||
export * from './primitives/shadcn-io/skeleton'
|
||||
export * from './primitives/shadcn-io/tabs'
|
||||
// Composite Components (复合组件)
|
||||
// 暂无复合组件
|
||||
|
||||
// UI Components (shadcn)
|
||||
export * from './ui/button'
|
||||
export * from './ui/command'
|
||||
export * from './ui/dialog'
|
||||
export * from './ui/popover'
|
||||
export * from './ui/shadcn-io/dropzone'
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Search } from 'lucide-react'
|
||||
import { motion } from 'motion/react'
|
||||
import React, { memo, useCallback, useEffect, useRef, useState } from 'react'
|
||||
|
||||
import { Tooltip } from '../../primitives/tooltip'
|
||||
import { Tooltip } from '../../base/Tooltip'
|
||||
|
||||
interface CollapsibleSearchBarProps {
|
||||
onSearch: (text: string) => void
|
||||
@@ -9,7 +9,7 @@ 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 Scrollbar from '../../layout/Scrollbar'
|
||||
import { droppableReorder } from './sort'
|
||||
|
||||
export interface DraggableVirtualListRef {
|
||||
@@ -1,7 +1,7 @@
|
||||
// Original path: src/renderer/src/components/TooltipIcons/HelpTooltip.tsx
|
||||
import { HelpCircle } from 'lucide-react'
|
||||
|
||||
import { Tooltip } from '../../primitives/tooltip'
|
||||
import { Tooltip } from '../../base/Tooltip'
|
||||
import type { IconTooltipProps } from './types'
|
||||
|
||||
export const HelpTooltip = ({ iconProps, ...rest }: IconTooltipProps) => {
|
||||
@@ -1,7 +1,7 @@
|
||||
// Original: src/renderer/src/components/TooltipIcons/InfoTooltip.tsx
|
||||
import { Info } from 'lucide-react'
|
||||
|
||||
import { Tooltip } from '../../primitives/tooltip'
|
||||
import { Tooltip } from '../../base/Tooltip'
|
||||
import type { IconTooltipProps } from './types'
|
||||
|
||||
export const InfoTooltip = ({ iconProps, ...rest }: IconTooltipProps) => {
|
||||
@@ -1,7 +1,7 @@
|
||||
// Original path: src/renderer/src/components/TooltipIcons/WarnTooltip.tsx
|
||||
import { AlertTriangle } from 'lucide-react'
|
||||
|
||||
import { Tooltip } from '../../primitives/tooltip'
|
||||
import { Tooltip } from '../../base/Tooltip'
|
||||
import type { IconTooltipProps } from './types'
|
||||
|
||||
export const WarnTooltip = ({ iconProps, ...rest }: IconTooltipProps) => {
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { LucideProps } from 'lucide-react'
|
||||
|
||||
import type { TooltipProps } from '../../primitives/tooltip'
|
||||
import type { TooltipProps } from '../../base/Tooltip'
|
||||
|
||||
export interface IconTooltipProps extends TooltipProps {
|
||||
iconProps?: LucideProps
|
||||
@@ -1,8 +1,8 @@
|
||||
// Original path: src/renderer/src/components/Preview/ImageToolButton.tsx
|
||||
import { memo } from 'react'
|
||||
|
||||
import { Button } from '../../primitives/button'
|
||||
import { Tooltip } from '../../primitives/tooltip'
|
||||
import Button from '../../base/Button'
|
||||
import { Tooltip } from '../../base/Tooltip'
|
||||
|
||||
interface ImageToolButtonProps {
|
||||
tooltip: string
|
||||
@@ -13,7 +13,7 @@ interface ImageToolButtonProps {
|
||||
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}>
|
||||
<Button radius="full" isIconOnly onPress={onPress} aria-label={tooltip}>
|
||||
{icon}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
@@ -1,7 +0,0 @@
|
||||
import { cn } from '@cherrystudio/ui/utils'
|
||||
|
||||
function Skeleton({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return <div data-slot="skeleton" className={cn('bg-accent animate-pulse rounded-md', className)} {...props} />
|
||||
}
|
||||
|
||||
export { Skeleton }
|
||||
@@ -1,39 +0,0 @@
|
||||
import { cn } from '@cherrystudio/ui/utils'
|
||||
import * as TabsPrimitive from '@radix-ui/react-tabs'
|
||||
import * as React from 'react'
|
||||
|
||||
function Tabs({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Root>) {
|
||||
return <TabsPrimitive.Root data-slot="tabs" className={cn('flex flex-col gap-2', className)} {...props} />
|
||||
}
|
||||
|
||||
function TabsList({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.List>) {
|
||||
return (
|
||||
<TabsPrimitive.List
|
||||
data-slot="tabs-list"
|
||||
className={cn(
|
||||
'bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TabsTrigger({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
|
||||
return (
|
||||
<TabsPrimitive.Trigger
|
||||
data-slot="tabs-trigger"
|
||||
className={cn(
|
||||
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TabsContent({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Content>) {
|
||||
return <TabsPrimitive.Content data-slot="tabs-content" className={cn('flex-1 outline-none', className)} {...props} />
|
||||
}
|
||||
|
||||
export { Tabs, TabsContent, TabsList, TabsTrigger }
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle
|
||||
} from '@cherrystudio/ui/components/primitives/dialog'
|
||||
} from '@cherrystudio/ui/components/ui/dialog'
|
||||
import { cn } from '@cherrystudio/ui/utils'
|
||||
import { Command as CommandPrimitive } from 'cmdk'
|
||||
import { SearchIcon } from 'lucide-react'
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { Button } from '@cherrystudio/ui/components/primitives/button'
|
||||
import { Button } from '@cherrystudio/ui/components/ui/button'
|
||||
import { cn } from '@cherrystudio/ui/utils/index'
|
||||
import { UploadIcon } from 'lucide-react'
|
||||
import type { ReactNode } from 'react'
|
||||
@@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react'
|
||||
import { Button } from '../../../src/components'
|
||||
|
||||
const meta: Meta<typeof Button> = {
|
||||
title: 'Components/Primitives/Button',
|
||||
title: 'Components/Base/Button',
|
||||
component: Button,
|
||||
parameters: {
|
||||
layout: 'centered'
|
||||
@@ -12,16 +12,30 @@ const meta: Meta<typeof Button> = {
|
||||
argTypes: {
|
||||
variant: {
|
||||
control: { type: 'select' },
|
||||
options: ['default', 'destructive', 'outline', 'secondary', 'ghost', 'link']
|
||||
options: ['solid', 'bordered', 'light', 'flat', 'faded', 'shadow', 'ghost']
|
||||
},
|
||||
color: {
|
||||
control: { type: 'select' },
|
||||
options: ['default', 'primary', 'secondary', 'success', 'warning', 'danger']
|
||||
},
|
||||
size: {
|
||||
control: { type: 'select' },
|
||||
options: ['default', 'sm', 'lg', 'icon', 'icon-sm', 'icon-lg']
|
||||
options: ['sm', 'md', 'lg']
|
||||
},
|
||||
disabled: {
|
||||
radius: {
|
||||
control: { type: 'select' },
|
||||
options: ['none', 'sm', 'md', 'lg', 'full']
|
||||
},
|
||||
isDisabled: {
|
||||
control: { type: 'boolean' }
|
||||
},
|
||||
asChild: {
|
||||
isLoading: {
|
||||
control: { type: 'boolean' }
|
||||
},
|
||||
fullWidth: {
|
||||
control: { type: 'boolean' }
|
||||
},
|
||||
isIconOnly: {
|
||||
control: { type: 'boolean' }
|
||||
}
|
||||
}
|
||||
@@ -41,12 +55,27 @@ export const Default: Story = {
|
||||
export const Variants: Story = {
|
||||
render: () => (
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<Button variant="default">Default</Button>
|
||||
<Button variant="secondary">Secondary</Button>
|
||||
<Button variant="destructive">Destructive</Button>
|
||||
<Button variant="outline">Outline</Button>
|
||||
<Button variant="solid">Solid</Button>
|
||||
<Button variant="bordered">Bordered</Button>
|
||||
<Button variant="light">Light</Button>
|
||||
<Button variant="flat">Flat</Button>
|
||||
<Button variant="faded">Faded</Button>
|
||||
<Button variant="shadow">Shadow</Button>
|
||||
<Button variant="ghost">Ghost</Button>
|
||||
<Button variant="link">Link</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 不同颜色
|
||||
export const Colors: Story = {
|
||||
render: () => (
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<Button color="default">Default</Button>
|
||||
<Button color="primary">Primary</Button>
|
||||
<Button color="secondary">Secondary</Button>
|
||||
<Button color="success">Success</Button>
|
||||
<Button color="warning">Warning</Button>
|
||||
<Button color="danger">Danger</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -54,21 +83,23 @@ export const Variants: Story = {
|
||||
// 不同尺寸
|
||||
export const Sizes: Story = {
|
||||
render: () => (
|
||||
<div className="flex gap-2 items-center flex-wrap">
|
||||
<div className="flex gap-2 items-center">
|
||||
<Button size="sm">Small</Button>
|
||||
<Button size="default">Default</Button>
|
||||
<Button size="md">Medium</Button>
|
||||
<Button size="lg">Large</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 图标按钮
|
||||
export const IconButtons: Story = {
|
||||
// 不同圆角
|
||||
export const Radius: Story = {
|
||||
render: () => (
|
||||
<div className="flex gap-2 items-center flex-wrap">
|
||||
<Button size="icon-sm">🔍</Button>
|
||||
<Button size="icon">🔍</Button>
|
||||
<Button size="icon-lg">🔍</Button>
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<Button radius="none">None</Button>
|
||||
<Button radius="sm">Small</Button>
|
||||
<Button radius="md">Medium</Button>
|
||||
<Button radius="lg">Large</Button>
|
||||
<Button radius="full">Full</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -78,7 +109,8 @@ export const States: Story = {
|
||||
render: () => (
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<Button>Normal</Button>
|
||||
<Button disabled>Disabled</Button>
|
||||
<Button isDisabled>Disabled</Button>
|
||||
<Button isLoading>Loading</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -87,15 +119,9 @@ export const States: Story = {
|
||||
export const WithIcons: Story = {
|
||||
render: () => (
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<Button>
|
||||
<span className="mr-2">📧</span>
|
||||
Email
|
||||
</Button>
|
||||
<Button>
|
||||
Next
|
||||
<span className="ml-2">→</span>
|
||||
</Button>
|
||||
<Button size="icon">🔍</Button>
|
||||
<Button startContent={<span>📧</span>}>Email</Button>
|
||||
<Button endContent={<span>→</span>}>Next</Button>
|
||||
<Button isIconOnly>🔍</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -104,7 +130,9 @@ export const WithIcons: Story = {
|
||||
export const FullWidth: Story = {
|
||||
render: () => (
|
||||
<div className="w-96">
|
||||
<Button className="w-full">Full Width Button</Button>
|
||||
<Button fullWidth color="primary">
|
||||
Full Width Button
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -113,45 +141,10 @@ export const FullWidth: Story = {
|
||||
export const Interactive: Story = {
|
||||
render: () => (
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<Button onClick={() => alert('Button clicked!')}>Click Me</Button>
|
||||
<Button onClick={() => console.log('Primary action')} variant="default">
|
||||
<Button onPress={() => alert('Button pressed!')}>Click Me</Button>
|
||||
<Button onPress={() => console.log('Primary action')} color="primary" variant="solid">
|
||||
Primary Action
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 组合示例
|
||||
export const Combinations: Story = {
|
||||
render: () => (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<Button variant="default" size="sm">
|
||||
Small Default
|
||||
</Button>
|
||||
<Button variant="destructive" size="sm">
|
||||
Small Destructive
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
Small Outline
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<Button variant="default">Default</Button>
|
||||
<Button variant="destructive">Destructive</Button>
|
||||
<Button variant="outline">Outline</Button>
|
||||
</div>
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<Button variant="default" size="lg">
|
||||
Large Default
|
||||
</Button>
|
||||
<Button variant="destructive" size="lg">
|
||||
Large Destructive
|
||||
</Button>
|
||||
<Button variant="outline" size="lg">
|
||||
Large Outline
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
|
||||
import CopyButton from '../../../src/components/primitives/copyButton'
|
||||
import CopyButton from '../../../src/components/base/CopyButton'
|
||||
|
||||
const meta: Meta<typeof CopyButton> = {
|
||||
title: 'Base/CopyButton',
|
||||
|
||||
459
packages/ui/stories/components/base/CustomCollapse.stories.tsx
Normal file
459
packages/ui/stories/components/base/CustomCollapse.stories.tsx
Normal file
@@ -0,0 +1,459 @@
|
||||
import { Button } from '@heroui/react'
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
import { AlertTriangle, CreditCard, Info, Monitor, Settings, Shield } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
|
||||
import CustomCollapse, { Accordion, AccordionItem } from '../../../src/components/base/CustomCollapse'
|
||||
|
||||
const meta: Meta<typeof CustomCollapse> = {
|
||||
title: 'Base/CustomCollapse',
|
||||
component: CustomCollapse,
|
||||
parameters: {
|
||||
layout: 'padded'
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
argTypes: {
|
||||
children: {
|
||||
control: false,
|
||||
description: '面板内容'
|
||||
},
|
||||
accordionProps: {
|
||||
control: false,
|
||||
description: 'Accordion 组件的属性'
|
||||
},
|
||||
accordionItemProps: {
|
||||
control: false,
|
||||
description: 'AccordionItem 组件的属性'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
// 基础用法
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
accordionItemProps: {
|
||||
title: '默认折叠面板'
|
||||
},
|
||||
children: (
|
||||
<div className="p-4">
|
||||
<p>这是折叠面板的内容。</p>
|
||||
<p>可以包含任何内容,包括文本、图片、表单等。</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 带副标题
|
||||
export const WithSubtitle: Story = {
|
||||
args: {
|
||||
accordionProps: {
|
||||
defaultExpandedKeys: ['1']
|
||||
},
|
||||
accordionItemProps: {
|
||||
title: '带副标题的折叠面板',
|
||||
subtitle: <span className="text-sm text-gray-500">这是副标题内容</span>
|
||||
},
|
||||
children: (
|
||||
<div className="p-4">
|
||||
<p>面板内容</p>
|
||||
<p>可以在 subtitle 属性中设置副标题</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// HeroUI 样式变体
|
||||
export const VariantLight: Story = {
|
||||
args: {
|
||||
accordionProps: {
|
||||
variant: 'light'
|
||||
},
|
||||
accordionItemProps: {
|
||||
title: 'Light 变体'
|
||||
},
|
||||
children: (
|
||||
<div className="p-4">
|
||||
<p>这是 HeroUI 的 Light 变体样式。</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export const VariantShadow: Story = {
|
||||
args: {
|
||||
accordionProps: {
|
||||
variant: 'shadow',
|
||||
className: 'p-2'
|
||||
},
|
||||
accordionItemProps: {
|
||||
title: 'Shadow 变体',
|
||||
subtitle: '带阴影的面板样式'
|
||||
},
|
||||
children: (
|
||||
<div className="p-4">
|
||||
<p>这是 HeroUI 的 Shadow 变体样式。</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export const VariantBordered: Story = {
|
||||
args: {
|
||||
accordionProps: {
|
||||
variant: 'bordered'
|
||||
},
|
||||
accordionItemProps: {
|
||||
title: 'Bordered 变体(默认)'
|
||||
},
|
||||
children: (
|
||||
<div className="p-4">
|
||||
<p>这是 HeroUI 的 Bordered 变体样式。</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export const VariantSplitted: Story = {
|
||||
args: {
|
||||
accordionProps: {
|
||||
variant: 'splitted'
|
||||
},
|
||||
accordionItemProps: {
|
||||
title: 'Splitted 变体'
|
||||
},
|
||||
children: (
|
||||
<div className="p-4">
|
||||
<p>这是 HeroUI 的 Splitted 变体样式。</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 富内容标题
|
||||
export const RichLabel: Story = {
|
||||
args: {
|
||||
accordionItemProps: {
|
||||
title: (
|
||||
<div className="flex items-center gap-2">
|
||||
<Settings className="text-default-500" size={20} />
|
||||
<span>设置面板</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
children: (
|
||||
<div className="p-4">
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span>通知设置</span>
|
||||
<Button size="sm" variant="flat">
|
||||
开启
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span>自动更新</span>
|
||||
<Button size="sm" variant="flat">
|
||||
关闭
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 带警告提示
|
||||
export const WithWarning: Story = {
|
||||
args: {
|
||||
accordionItemProps: {
|
||||
title: (
|
||||
<div className="flex items-center gap-2">
|
||||
<Monitor className="text-primary" size={20} />
|
||||
<span>连接的设备</span>
|
||||
</div>
|
||||
),
|
||||
subtitle: (
|
||||
<p className="flex">
|
||||
2个问题需要<span className="text-primary ml-1">立即修复</span>
|
||||
</p>
|
||||
)
|
||||
},
|
||||
children: (
|
||||
<div className="p-4">
|
||||
<p className="text-small">检测到以下设备连接异常:</p>
|
||||
<ul className="list-disc list-inside mt-2 text-small space-y-1">
|
||||
<li>外部显示器连接不稳定</li>
|
||||
<li>蓝牙键盘配对失败</li>
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 禁用状态
|
||||
export const Disabled: Story = {
|
||||
args: {
|
||||
accordionProps: {
|
||||
isDisabled: true,
|
||||
defaultExpandedKeys: ['1']
|
||||
},
|
||||
accordionItemProps: {
|
||||
title: '禁用的折叠面板'
|
||||
},
|
||||
children: (
|
||||
<div className="p-4">
|
||||
<p>这个面板被禁用了,无法操作。</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 受控模式
|
||||
export const ControlledMode: Story = {
|
||||
render: function ControlledMode() {
|
||||
const [selectedKeys, setSelectedKeys] = useState<Set<string>>(new Set(['1']))
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" onPress={() => setSelectedKeys(new Set(['1']))} color="primary">
|
||||
展开
|
||||
</Button>
|
||||
<Button size="sm" onPress={() => setSelectedKeys(new Set())} color="default">
|
||||
收起
|
||||
</Button>
|
||||
</div>
|
||||
<CustomCollapse
|
||||
accordionProps={{
|
||||
selectedKeys,
|
||||
onSelectionChange: (keys) => {
|
||||
if (keys !== 'all') {
|
||||
setSelectedKeys(keys as Set<string>)
|
||||
}
|
||||
}
|
||||
}}
|
||||
accordionItemProps={{
|
||||
title: '受控的折叠面板'
|
||||
}}>
|
||||
<div className="p-4">
|
||||
<p>这是一个受控的折叠面板</p>
|
||||
<p>通过按钮控制展开和收起状态</p>
|
||||
</div>
|
||||
</CustomCollapse>
|
||||
<div className="text-sm text-gray-600">当前状态:{selectedKeys.size > 0 ? '展开' : '收起'}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 多个单面板组合
|
||||
export const MultipleSinglePanels: Story = {
|
||||
render: () => (
|
||||
<div className="space-y-4">
|
||||
<CustomCollapse accordionProps={{ defaultExpandedKeys: ['1'] }} accordionItemProps={{ title: '第一个面板' }}>
|
||||
<div className="p-4">
|
||||
<p>第一个面板的内容</p>
|
||||
</div>
|
||||
</CustomCollapse>
|
||||
<CustomCollapse
|
||||
accordionItemProps={{
|
||||
title: '第二个面板',
|
||||
subtitle: '带副标题'
|
||||
}}>
|
||||
<div className="p-4">
|
||||
<p>第二个面板的内容</p>
|
||||
</div>
|
||||
</CustomCollapse>
|
||||
<CustomCollapse accordionProps={{ isDisabled: true }} accordionItemProps={{ title: '第三个面板(禁用)' }}>
|
||||
<div className="p-4">
|
||||
<p>这个面板被禁用了</p>
|
||||
</div>
|
||||
</CustomCollapse>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 使用原生 HeroUI Accordion 的多面板示例
|
||||
export const NativeAccordionMultiple: Story = {
|
||||
render: () => (
|
||||
<div className="max-w-lg">
|
||||
<h3 className="text-lg font-medium mb-4">原生 HeroUI Accordion 多面板</h3>
|
||||
<Accordion variant="shadow" className="p-2 flex flex-col gap-1" defaultExpandedKeys={['1']}>
|
||||
<AccordionItem
|
||||
key="1"
|
||||
title="连接的设备"
|
||||
startContent={<Monitor className="text-primary" size={20} />}
|
||||
subtitle={
|
||||
<p className="flex">
|
||||
2个问题需要<span className="text-primary ml-1">立即修复</span>
|
||||
</p>
|
||||
}>
|
||||
<div className="p-4">
|
||||
<p className="text-small">设备连接状态监控</p>
|
||||
</div>
|
||||
</AccordionItem>
|
||||
<AccordionItem
|
||||
key="2"
|
||||
title="应用权限"
|
||||
startContent={<Shield className="text-default-500" size={20} />}
|
||||
subtitle="3个应用有读取权限">
|
||||
<div className="p-4">
|
||||
<p className="text-small">管理应用的系统权限设置</p>
|
||||
</div>
|
||||
</AccordionItem>
|
||||
<AccordionItem
|
||||
key="3"
|
||||
title="待办任务"
|
||||
startContent={<Info className="text-warning" size={20} />}
|
||||
subtitle={<span className="text-warning">请完善您的个人资料</span>}>
|
||||
<div className="p-4">
|
||||
<p className="text-small">您还有一些信息需要完善</p>
|
||||
</div>
|
||||
</AccordionItem>
|
||||
<AccordionItem
|
||||
key="4"
|
||||
title={
|
||||
<p className="flex gap-1 items-center">
|
||||
卡片已过期
|
||||
<span className="text-default-400 text-small">*4812</span>
|
||||
</p>
|
||||
}
|
||||
startContent={<CreditCard className="text-danger" size={20} />}
|
||||
subtitle={<span className="text-danger">请立即更新</span>}>
|
||||
<div className="p-4">
|
||||
<p className="text-small text-danger">您的信用卡已过期,请更新支付信息</p>
|
||||
</div>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 富内容面板
|
||||
export const RichContent: Story = {
|
||||
args: {
|
||||
accordionItemProps: {
|
||||
title: (
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<div className="flex items-center gap-2">
|
||||
<Info className="text-default-500" size={20} />
|
||||
<span>详细信息</span>
|
||||
</div>
|
||||
<div className="flex gap-2" onClick={(e) => e.stopPropagation()}>
|
||||
<Button size="sm" variant="flat" color="primary">
|
||||
保存
|
||||
</Button>
|
||||
<Button size="sm" variant="flat">
|
||||
取消
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
children: (
|
||||
<div className="p-4 space-y-4">
|
||||
<div>
|
||||
<h4 className="font-medium mb-2">基本信息</h4>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">名称</label>
|
||||
<input
|
||||
type="text"
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md"
|
||||
placeholder="请输入名称"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">类型</label>
|
||||
<select className="w-full px-3 py-2 border border-gray-300 rounded-md">
|
||||
<option>选择类型</option>
|
||||
<option>类型 A</option>
|
||||
<option>类型 B</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">描述</label>
|
||||
<textarea className="w-full px-3 py-2 border border-gray-300 rounded-md" rows={3} placeholder="请输入描述" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 自定义样式
|
||||
export const CustomStyles: Story = {
|
||||
args: {
|
||||
accordionProps: {
|
||||
style: {
|
||||
backgroundColor: 'rgba(255, 193, 7, 0.1)',
|
||||
borderColor: 'var(--color-warning)'
|
||||
}
|
||||
},
|
||||
accordionItemProps: {
|
||||
title: (
|
||||
<div className="flex items-center gap-2">
|
||||
<AlertTriangle className="text-warning" size={16} />
|
||||
<span>警告面板</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
children: (
|
||||
<div className="p-4 bg-warning-50 dark:bg-warning-900/20">
|
||||
<p className="text-warning-800 dark:text-warning-200">这是一个带有自定义样式的警告面板。</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 原生 HeroUI Accordion 多面板受控模式
|
||||
export const NativeAccordionControlled: Story = {
|
||||
render: function NativeAccordionControlled() {
|
||||
const [activeKeys, setActiveKeys] = useState<Set<string>>(new Set(['1']))
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" onPress={() => setActiveKeys(new Set(['1', '2', '3']))} color="primary">
|
||||
全部展开
|
||||
</Button>
|
||||
<Button size="sm" onPress={() => setActiveKeys(new Set())} color="default">
|
||||
全部收起
|
||||
</Button>
|
||||
<Button size="sm" onPress={() => setActiveKeys(new Set(['2']))} color="default">
|
||||
只展开第二个
|
||||
</Button>
|
||||
</div>
|
||||
<Accordion
|
||||
selectedKeys={activeKeys}
|
||||
onSelectionChange={(keys) => {
|
||||
if (keys !== 'all') {
|
||||
setActiveKeys(keys as Set<string>)
|
||||
}
|
||||
}}>
|
||||
<AccordionItem key="1" title="受控面板 1">
|
||||
<div className="p-4">
|
||||
<p>第一个面板的内容</p>
|
||||
</div>
|
||||
</AccordionItem>
|
||||
<AccordionItem key="2" title="受控面板 2">
|
||||
<div className="p-4">
|
||||
<p>第二个面板的内容</p>
|
||||
</div>
|
||||
</AccordionItem>
|
||||
<AccordionItem key="3" title="受控面板 3">
|
||||
<div className="p-4">
|
||||
<p>第三个面板的内容</p>
|
||||
</div>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
<div className="text-sm text-gray-600">
|
||||
当前展开的面板:{activeKeys.size > 0 ? Array.from(activeKeys).join(', ') : '无'}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import type { Meta, StoryObj } from '@storybook/react-vite'
|
||||
import { AlertTriangleIcon, StarIcon } from 'lucide-react'
|
||||
import { action } from 'storybook/actions'
|
||||
|
||||
import CustomTag from '../../../src/components/primitives/customTag'
|
||||
import CustomTag from '../../../src/components/base/CustomTag'
|
||||
|
||||
const meta: Meta<typeof CustomTag> = {
|
||||
title: 'Base/CustomTag',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
|
||||
import DividerWithText from '../../../src/components/primitives/dividerWithText'
|
||||
import DividerWithText from '../../../src/components/base/DividerWithText'
|
||||
|
||||
const meta: Meta<typeof DividerWithText> = {
|
||||
title: 'Base/DividerWithText',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
|
||||
import EmojiAvatar from '../../../src/components/primitives/Avatar/EmojiAvatar'
|
||||
import EmojiAvatar from '../../../src/components/base/Avatar/EmojiAvatar'
|
||||
|
||||
const meta: Meta<typeof EmojiAvatar> = {
|
||||
title: 'Display/EmojiAvatar',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
|
||||
import EmojiIcon from '../../../src/components/primitives/emojiIcon'
|
||||
import EmojiIcon from '../../../src/components/base/EmojiIcon'
|
||||
|
||||
const meta: Meta<typeof EmojiIcon> = {
|
||||
title: 'Base/EmojiIcon',
|
||||
|
||||
@@ -2,8 +2,8 @@ import { Button } from '@heroui/react'
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
import { useState } from 'react'
|
||||
|
||||
import type { CustomFallbackProps } from '../../../src/components/primitives/ErrorBoundary'
|
||||
import { ErrorBoundary } from '../../../src/components/primitives/ErrorBoundary'
|
||||
import type { CustomFallbackProps } from '../../../src/components/base/ErrorBoundary'
|
||||
import { ErrorBoundary } from '../../../src/components/base/ErrorBoundary'
|
||||
|
||||
// 错误组件 - 用于触发错误
|
||||
const ThrowErrorComponent = ({ shouldThrow = false, errorMessage = '这是一个模拟错误' }) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
|
||||
import IndicatorLight from '../../../src/components/primitives/indicatorLight'
|
||||
import IndicatorLight from '../../../src/components/base/IndicatorLight'
|
||||
|
||||
const meta: Meta<typeof IndicatorLight> = {
|
||||
title: 'Base/IndicatorLight',
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Button } from '@heroui/react'
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
import { useState } from 'react'
|
||||
|
||||
import Spinner from '../../../src/components/primitives/spinner'
|
||||
import Spinner from '../../../src/components/base/Spinner'
|
||||
|
||||
const meta: Meta<typeof Spinner> = {
|
||||
title: 'Base/Spinner',
|
||||
|
||||
176
packages/ui/stories/components/base/StatusTag.stories.tsx
Normal file
176
packages/ui/stories/components/base/StatusTag.stories.tsx
Normal file
@@ -0,0 +1,176 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
|
||||
import { ErrorTag, InfoTag, StatusTag, SuccessTag, WarnTag } from '../../../src/components/base/StatusTag'
|
||||
|
||||
const meta: Meta<typeof StatusTag> = {
|
||||
title: 'Base/StatusTag',
|
||||
component: StatusTag,
|
||||
parameters: {
|
||||
layout: 'centered'
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
argTypes: {
|
||||
type: {
|
||||
control: { type: 'select' },
|
||||
options: ['success', 'error', 'warning', 'info']
|
||||
},
|
||||
iconSize: { control: { type: 'range', min: 10, max: 24, step: 1 } },
|
||||
message: { control: 'text' },
|
||||
color: { control: 'color' }
|
||||
}
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
// Default
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
type: 'success',
|
||||
message: 'Success'
|
||||
}
|
||||
}
|
||||
|
||||
// All Types
|
||||
export const AllTypes: Story = {
|
||||
render: () => (
|
||||
<div className="flex flex-col gap-3">
|
||||
<StatusTag type="success" message="Success message" />
|
||||
<StatusTag type="error" message="Error message" />
|
||||
<StatusTag type="warning" message="Warning message" />
|
||||
<StatusTag type="info" message="Info message" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Convenience Components
|
||||
export const ConvenienceComponents: Story = {
|
||||
render: () => (
|
||||
<div className="flex flex-col gap-3">
|
||||
<SuccessTag message="Operation completed" />
|
||||
<ErrorTag message="Operation failed" />
|
||||
<WarnTag message="Please check this" />
|
||||
<InfoTag message="Additional information" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Different Icon Sizes
|
||||
export const IconSizes: Story = {
|
||||
render: () => (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<StatusTag type="success" iconSize={10} message="Small icon" />
|
||||
<StatusTag type="success" iconSize={14} message="Default icon" />
|
||||
<StatusTag type="success" iconSize={18} message="Large icon" />
|
||||
<StatusTag type="success" iconSize={24} message="Extra large icon" />
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<ErrorTag iconSize={10} message="Small icon" />
|
||||
<ErrorTag iconSize={14} message="Default icon" />
|
||||
<ErrorTag iconSize={18} message="Large icon" />
|
||||
<ErrorTag iconSize={24} message="Extra large icon" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Custom Colors
|
||||
export const CustomColors: Story = {
|
||||
render: () => (
|
||||
<div className="flex flex-col gap-3">
|
||||
<StatusTag type="success" message="Custom purple" color="#8B5CF6" />
|
||||
<StatusTag type="error" message="Custom blue" color="#3B82F6" />
|
||||
<StatusTag type="warning" message="Custom green" color="#10B981" />
|
||||
<StatusTag type="info" message="Custom pink" color="#EC4899" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// In Context
|
||||
export const InContext: Story = {
|
||||
render: () => (
|
||||
<div className="space-y-4">
|
||||
<div className="rounded-lg border border-gray-200 p-4">
|
||||
<h3 className="mb-2 font-semibold">Form Submission</h3>
|
||||
<p className="mb-3 text-sm text-gray-600">Your form has been processed.</p>
|
||||
<SuccessTag message="Form submitted successfully" />
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border border-gray-200 p-4">
|
||||
<h3 className="mb-2 font-semibold">Validation Error</h3>
|
||||
<p className="mb-3 text-sm text-gray-600">Please fix the following issues:</p>
|
||||
<ErrorTag message="Invalid email format" />
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border border-gray-200 p-4">
|
||||
<h3 className="mb-2 font-semibold">System Status</h3>
|
||||
<div className="space-y-2">
|
||||
<SuccessTag message="Database connected" />
|
||||
<WarnTag message="High memory usage" />
|
||||
<ErrorTag message="Email service down" />
|
||||
<InfoTag message="Last backup: 2 hours ago" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Use Cases
|
||||
export const UseCases: Story = {
|
||||
render: () => (
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<div className="space-y-2">
|
||||
<h4 className="font-medium">Success States</h4>
|
||||
<div className="space-y-2">
|
||||
<SuccessTag message="Saved" />
|
||||
<SuccessTag message="Published" />
|
||||
<SuccessTag message="Deployed" />
|
||||
<SuccessTag message="Verified" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<h4 className="font-medium">Error States</h4>
|
||||
<div className="space-y-2">
|
||||
<ErrorTag message="Failed" />
|
||||
<ErrorTag message="Timeout" />
|
||||
<ErrorTag message="Not found" />
|
||||
<ErrorTag message="Access denied" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<h4 className="font-medium">Warning States</h4>
|
||||
<div className="space-y-2">
|
||||
<WarnTag message="Deprecated" />
|
||||
<WarnTag message="Limited" />
|
||||
<WarnTag message="Expiring soon" />
|
||||
<WarnTag message="Low balance" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<h4 className="font-medium">Info States</h4>
|
||||
<div className="space-y-2">
|
||||
<InfoTag message="New" />
|
||||
<InfoTag message="Beta" />
|
||||
<InfoTag message="Preview" />
|
||||
<InfoTag message="Optional" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Long Messages
|
||||
export const LongMessages: Story = {
|
||||
render: () => (
|
||||
<div className="max-w-md space-y-3">
|
||||
<SuccessTag message="Your request has been successfully processed and saved to the database" />
|
||||
<ErrorTag message="Unable to connect to the server. Please check your network connection and try again" />
|
||||
<WarnTag message="This feature will be deprecated in the next major version. Please migrate to the new API" />
|
||||
<InfoTag message="Additional information about this feature can be found in the documentation at docs.example.com" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user