mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-04-28 03:30:40 +00:00
feat(webui): migrate icons, Tooltip, WaitingMessage from vscode-ide-companion
- Move icon components (FileIcons, EditIcons, NavigationIcons, StatusIcons, SpecialIcons, StopIcon) from vscode-ide-companion to webui package - Migrate Tooltip component with CSS variable theming support - Migrate WaitingMessage and InterruptedMessage components - Enhance Button component with forwardRef, new variants (ghost, outline), loading state, and icon support - Enhance Input component with forwardRef, error state, label, and helper text - Update vscode-ide-companion to import components from @qwen-code/webui - Remove replaced local components from vscode-ide-companion - Add skipLibCheck to vscode-ide-companion tsconfig for type compatibility
This commit is contained in:
parent
af76450dee
commit
71570540cc
45 changed files with 1049 additions and 308 deletions
|
|
@ -37,12 +37,11 @@ import {
|
|||
UserMessage,
|
||||
AssistantMessage,
|
||||
ThinkingMessage,
|
||||
WaitingMessage,
|
||||
InterruptedMessage,
|
||||
} from './components/messages/index.js';
|
||||
import { WaitingMessage, InterruptedMessage } from '@qwen-code/webui';
|
||||
import { InputForm } from './components/layout/InputForm.js';
|
||||
import { SessionSelector } from './components/layout/SessionSelector.js';
|
||||
import { FileIcon, UserIcon } from './components/icons/index.js';
|
||||
import { FileIcon, UserIcon } from '@qwen-code/webui';
|
||||
import { ApprovalMode, NEXT_APPROVAL_MODE } from '../types/acpTypes.js';
|
||||
import type { ApprovalModeValue } from '../types/approvalModeValueTypes.js';
|
||||
import type { PlanEntry, UsageStatsPayload } from '../types/chatTypes.js';
|
||||
|
|
|
|||
|
|
@ -1,61 +0,0 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
|
||||
interface TooltipProps {
|
||||
children: React.ReactNode;
|
||||
content: React.ReactNode;
|
||||
position?: 'top' | 'bottom' | 'left' | 'right';
|
||||
}
|
||||
|
||||
export const Tooltip: React.FC<TooltipProps> = ({
|
||||
children,
|
||||
content,
|
||||
position = 'top',
|
||||
}) => (
|
||||
<div className="relative inline-block">
|
||||
<div className="group relative">
|
||||
{children}
|
||||
<div
|
||||
className={`
|
||||
absolute z-50 px-2 py-1 text-xs rounded-md shadow-lg
|
||||
bg-[var(--app-primary-background)] border border-[var(--app-input-border)]
|
||||
text-[var(--app-primary-foreground)] whitespace-nowrap
|
||||
opacity-0 group-hover:opacity-100 transition-opacity duration-150
|
||||
-translate-x-1/2 left-1/2
|
||||
${
|
||||
position === 'top'
|
||||
? '-translate-y-1 bottom-full mb-1'
|
||||
: position === 'bottom'
|
||||
? 'translate-y-1 top-full mt-1'
|
||||
: position === 'left'
|
||||
? '-translate-x-full left-0 translate-y-[-50%] top-1/2'
|
||||
: 'translate-x-0 right-0 translate-y-[-50%] top-1/2'
|
||||
}
|
||||
pointer-events-none
|
||||
`}
|
||||
>
|
||||
{content}
|
||||
<div
|
||||
className={`
|
||||
absolute w-2 h-2 bg-[var(--app-primary-background)] border-l border-b border-[var(--app-input-border)]
|
||||
-rotate-45
|
||||
${
|
||||
position === 'top'
|
||||
? 'top-full left-1/2 -translate-x-1/2 -translate-y-1/2'
|
||||
: position === 'bottom'
|
||||
? 'bottom-full left-1/2 -translate-x-1/2 translate-y-1/2'
|
||||
: position === 'left'
|
||||
? 'right-full top-1/2 translate-x-1/2 -translate-y-1/2'
|
||||
: 'left-full top-1/2 -translate-x-1/2 -translate-y-1/2'
|
||||
}
|
||||
`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { ChevronDownIcon, PlusIcon } from '../icons/index.js';
|
||||
import { ChevronDownIcon, PlusIcon } from '@qwen-code/webui';
|
||||
|
||||
interface ChatHeaderProps {
|
||||
currentSessionTitle: string;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { Tooltip } from '../Tooltip.js';
|
||||
import { Tooltip } from '@qwen-code/webui';
|
||||
|
||||
interface ContextUsage {
|
||||
percentLeft: number;
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import {
|
|||
LinkIcon,
|
||||
ArrowUpIcon,
|
||||
StopIcon,
|
||||
} from '../icons/index.js';
|
||||
} from '@qwen-code/webui';
|
||||
import { CompletionMenu } from '../layout/CompletionMenu.js';
|
||||
import type { CompletionItem } from '../../../types/completionItemTypes.js';
|
||||
import { getApprovalModeInfoFromString } from '../../../types/acpTypes.js';
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import {
|
|||
getTimeAgo,
|
||||
groupSessionsByDate,
|
||||
} from '../../utils/sessionGrouping.js';
|
||||
import { SearchIcon } from '../icons/index.js';
|
||||
import { SearchIcon } from '@qwen-code/webui';
|
||||
|
||||
interface SessionSelectorProps {
|
||||
visible: boolean;
|
||||
|
|
|
|||
|
|
@ -1,38 +0,0 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
@import url('../Assistant/AssistantMessage.css');
|
||||
|
||||
/* Subtle shimmering highlight across the loading text */
|
||||
@keyframes waitingMessageShimmer {
|
||||
0% {
|
||||
background-position: -200% 0;
|
||||
}
|
||||
100% {
|
||||
background-position: 200% 0;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-text-shimmer {
|
||||
/* Use the theme foreground as the base color, with a moving light band */
|
||||
background-image: linear-gradient(
|
||||
90deg,
|
||||
var(--app-secondary-foreground) 0%,
|
||||
var(--app-secondary-foreground) 40%,
|
||||
rgba(255, 255, 255, 0.95) 50%,
|
||||
var(--app-secondary-foreground) 60%,
|
||||
var(--app-secondary-foreground) 100%
|
||||
);
|
||||
background-size: 200% 100%;
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent; /* text color comes from the gradient */
|
||||
animation: waitingMessageShimmer 1.6s linear infinite;
|
||||
}
|
||||
|
||||
.interrupted-item::after {
|
||||
display: none;
|
||||
}
|
||||
|
|
@ -7,5 +7,4 @@
|
|||
export { UserMessage } from './UserMessage.js';
|
||||
export { AssistantMessage } from './Assistant/AssistantMessage.js';
|
||||
export { ThinkingMessage } from './ThinkingMessage.js';
|
||||
export { WaitingMessage } from './Waiting/WaitingMessage.js';
|
||||
export { InterruptedMessage } from './Waiting/InterruptedMessage.js';
|
||||
// WaitingMessage and InterruptedMessage are now imported from @qwen-code/webui
|
||||
|
|
|
|||
|
|
@ -7,7 +7,8 @@
|
|||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "react",
|
||||
"sourceMap": true,
|
||||
"strict": true /* enable all strict type-checking options */
|
||||
"strict": true,
|
||||
"skipLibCheck": true
|
||||
/* Additional Checks */
|
||||
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
|
||||
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
|
||||
|
|
|
|||
|
|
@ -1,3 +1,9 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type { StorybookConfig } from '@storybook/react-vite';
|
||||
|
||||
import { dirname } from 'path';
|
||||
|
|
|
|||
|
|
@ -1,3 +1,9 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type { Preview } from '@storybook/react-vite';
|
||||
import './preview.css';
|
||||
|
||||
|
|
|
|||
210
packages/webui/README.md
Normal file
210
packages/webui/README.md
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
# @anthropic/webui
|
||||
|
||||
A shared React component library for Qwen Code applications, providing cross-platform UI components with consistent styling and behavior.
|
||||
|
||||
## Features
|
||||
|
||||
- **Cross-platform support**: Components work seamlessly across VS Code extension, web, and other platforms
|
||||
- **Platform Context**: Abstraction layer for platform-specific capabilities
|
||||
- **Tailwind CSS**: Shared styling preset for consistent design
|
||||
- **TypeScript**: Full type definitions for all components
|
||||
- **Storybook**: Interactive component documentation and development
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install @anthropic/webui
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```tsx
|
||||
import { Button, Input, Tooltip } from '@anthropic/webui';
|
||||
import { PlatformProvider } from '@anthropic/webui/context';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<PlatformProvider value={platformContext}>
|
||||
<Button variant="primary" onClick={handleClick}>
|
||||
Click me
|
||||
</Button>
|
||||
</PlatformProvider>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Components
|
||||
|
||||
### UI Components
|
||||
|
||||
#### Button
|
||||
|
||||
```tsx
|
||||
import { Button } from '@anthropic/webui';
|
||||
|
||||
<Button variant="primary" size="md" loading={false}>
|
||||
Submit
|
||||
</Button>;
|
||||
```
|
||||
|
||||
**Props:**
|
||||
|
||||
- `variant`: 'primary' | 'secondary' | 'danger' | 'ghost' | 'outline'
|
||||
- `size`: 'sm' | 'md' | 'lg'
|
||||
- `loading`: boolean
|
||||
- `leftIcon`: ReactNode
|
||||
- `rightIcon`: ReactNode
|
||||
- `fullWidth`: boolean
|
||||
|
||||
#### Input
|
||||
|
||||
```tsx
|
||||
import { Input } from '@anthropic/webui';
|
||||
|
||||
<Input
|
||||
label="Email"
|
||||
placeholder="Enter email"
|
||||
error={hasError}
|
||||
errorMessage="Invalid email"
|
||||
/>;
|
||||
```
|
||||
|
||||
**Props:**
|
||||
|
||||
- `size`: 'sm' | 'md' | 'lg'
|
||||
- `error`: boolean
|
||||
- `errorMessage`: string
|
||||
- `label`: string
|
||||
- `helperText`: string
|
||||
- `leftElement`: ReactNode
|
||||
- `rightElement`: ReactNode
|
||||
|
||||
#### Tooltip
|
||||
|
||||
```tsx
|
||||
import { Tooltip } from '@anthropic/webui';
|
||||
|
||||
<Tooltip content="Helpful tip">
|
||||
<span>Hover me</span>
|
||||
</Tooltip>;
|
||||
```
|
||||
|
||||
### Icons
|
||||
|
||||
```tsx
|
||||
import { FileIcon, FolderIcon, CheckIcon } from '@anthropic/webui/icons';
|
||||
|
||||
<FileIcon size={16} className="text-gray-500" />;
|
||||
```
|
||||
|
||||
Available icon categories:
|
||||
|
||||
- **FileIcons**: FileIcon, FolderIcon, SaveDocumentIcon
|
||||
- **StatusIcons**: CheckIcon, ErrorIcon, WarningIcon, LoadingIcon
|
||||
- **NavigationIcons**: ArrowLeftIcon, ArrowRightIcon, ChevronIcon
|
||||
- **EditIcons**: EditIcon, DeleteIcon, CopyIcon
|
||||
- **SpecialIcons**: SendIcon, StopIcon, CloseIcon
|
||||
|
||||
### Layout Components
|
||||
|
||||
- `Container`: Main layout wrapper
|
||||
- `Header`: Application header
|
||||
- `Footer`: Application footer
|
||||
- `Sidebar`: Side navigation
|
||||
- `Main`: Main content area
|
||||
|
||||
### Message Components
|
||||
|
||||
- `Message`: Chat message display
|
||||
- `MessageList`: List of messages
|
||||
- `MessageInput`: Message input field
|
||||
- `WaitingMessage`: Loading/waiting state
|
||||
- `InterruptedMessage`: Interrupted state display
|
||||
|
||||
## Platform Context
|
||||
|
||||
The Platform Context provides an abstraction layer for platform-specific capabilities:
|
||||
|
||||
```tsx
|
||||
import { PlatformProvider, usePlatform } from '@anthropic/webui/context';
|
||||
|
||||
const platformContext = {
|
||||
postMessage: (message) => vscode.postMessage(message),
|
||||
onMessage: (handler) => {
|
||||
window.addEventListener('message', handler);
|
||||
return () => window.removeEventListener('message', handler);
|
||||
},
|
||||
openFile: (path) => {
|
||||
/* platform-specific */
|
||||
},
|
||||
platform: 'vscode',
|
||||
};
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<PlatformProvider value={platformContext}>
|
||||
<YourApp />
|
||||
</PlatformProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function Component() {
|
||||
const { postMessage, platform } = usePlatform();
|
||||
// Use platform capabilities
|
||||
}
|
||||
```
|
||||
|
||||
## Tailwind Preset
|
||||
|
||||
Use the shared Tailwind preset for consistent styling:
|
||||
|
||||
```js
|
||||
// tailwind.config.js
|
||||
module.exports = {
|
||||
presets: [require('@anthropic/webui/tailwind.preset.cjs')],
|
||||
// your customizations
|
||||
};
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
### Running Storybook
|
||||
|
||||
```bash
|
||||
cd packages/webui
|
||||
npm run storybook
|
||||
```
|
||||
|
||||
### Building
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Type Checking
|
||||
|
||||
```bash
|
||||
npm run typecheck
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
packages/webui/
|
||||
├── src/
|
||||
│ ├── components/
|
||||
│ │ ├── icons/ # Icon components
|
||||
│ │ ├── layout/ # Layout components
|
||||
│ │ ├── messages/ # Message components
|
||||
│ │ └── ui/ # UI primitives
|
||||
│ ├── context/ # Platform context
|
||||
│ ├── hooks/ # Custom hooks
|
||||
│ └── types/ # Type definitions
|
||||
├── .storybook/ # Storybook config
|
||||
├── tailwind.preset.cjs # Shared Tailwind preset
|
||||
└── vite.config.ts # Build configuration
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
Apache-2.0
|
||||
|
|
@ -1,3 +1,9 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
// Example of how to use shared UI components
|
||||
// This would typically be integrated into existing components
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,11 @@
|
|||
"import": "./dist/index.js",
|
||||
"require": "./dist/index.cjs"
|
||||
},
|
||||
"./icons": {
|
||||
"types": "./dist/components/icons/index.d.ts",
|
||||
"import": "./dist/components/icons/index.js",
|
||||
"require": "./dist/components/icons/index.cjs"
|
||||
},
|
||||
"./tailwind.preset": "./tailwind.preset.cjs",
|
||||
"./styles.css": "./dist/styles.css"
|
||||
},
|
||||
|
|
|
|||
48
packages/webui/scripts/add-license-header.sh
Executable file
48
packages/webui/scripts/add-license-header.sh
Executable file
|
|
@ -0,0 +1,48 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Script to check and add license header to files in the packages/webui directory
|
||||
# If a file doesn't have the required license header, it will be added at the top
|
||||
# Excludes Markdown files and common build/dependency directories
|
||||
|
||||
LICENSE_HEADER="/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/"
|
||||
|
||||
# Directory to scan (relative to script location)
|
||||
TARGET_DIR="$(dirname "$0")/../"
|
||||
|
||||
# Find all JavaScript, TypeScript, CSS, HTML, and JSX/TSX files in the target directory, excluding Markdown files
|
||||
# Also exclude common build/dependency directories
|
||||
find "$TARGET_DIR" -type f \( -name "*.js" -o -name "*.jsx" -o -name "*.ts" -o -name "*.tsx" -o -name "*.cjs" -o -name "*.mjs" -o -name "*.css" -o -name "*.html" \) -not -name "*.md" \
|
||||
-not -path "*/node_modules/*" \
|
||||
-not -path "*/dist/*" \
|
||||
-not -path "*/build/*" \
|
||||
-not -path "*/coverage/*" \
|
||||
-not -path "*/.next/*" \
|
||||
-not -path "*/out/*" \
|
||||
-not -path "*/target/*" \
|
||||
-not -path "*/vendor/*" \
|
||||
-print0 | while IFS= read -r -d '' file; do
|
||||
# Skip the script file itself
|
||||
if [[ "$(basename "$file")" != "add-license-header.sh" ]]; then
|
||||
# Check if the file starts with the license header
|
||||
if ! head -n 5 "$file" | grep -Fq "@license"; then
|
||||
echo "Adding license header to: $file"
|
||||
|
||||
# Create a temporary file with the license header followed by the original content
|
||||
temp_file=$(mktemp)
|
||||
echo "$LICENSE_HEADER" > "$temp_file"
|
||||
echo "" >> "$temp_file" # Add an empty line after the license header
|
||||
cat "$file" >> "$temp_file"
|
||||
|
||||
# Move the temporary file to replace the original file
|
||||
mv "$temp_file" "$file"
|
||||
else
|
||||
echo "License header already present in: $file"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
echo "License header check and update completed."
|
||||
|
|
@ -1,3 +1,9 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,9 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
|
||||
interface CloseIconProps {
|
||||
|
|
@ -11,20 +17,20 @@ const CloseIcon: React.FC<CloseIconProps> = ({
|
|||
color = 'currentColor',
|
||||
className = '',
|
||||
}) => (
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke={color}
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={className}
|
||||
>
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
);
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke={color}
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={className}
|
||||
>
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default CloseIcon;
|
||||
|
|
|
|||
|
|
@ -213,3 +213,193 @@ export const OpenDiffIcon: React.FC<IconProps> = ({
|
|||
<path d="M13.5 7l-4-4v3h-6v2h6v3l4-4z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
/**
|
||||
* Undo edit icon (16x16)
|
||||
* Used for undoing edits in diff views
|
||||
*/
|
||||
export const UndoIcon: React.FC<IconProps> = ({
|
||||
size = 16,
|
||||
className,
|
||||
...props
|
||||
}) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
width={size}
|
||||
height={size}
|
||||
className={className}
|
||||
aria-hidden="true"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M9 10.6667L12.3333 14L9 17.3333M12.3333 14H4.66667C3.56112 14 2.66667 13.1056 2.66667 12V4.66667C2.66667 3.56112 3.56112 2.66667 4.66667 2.66667H13.3333C14.4389 2.66667 15.3333 3.56112 15.3333 4.66667V8.66667"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.33333"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
/**
|
||||
* Redo edit icon (16x16)
|
||||
* Used for redoing edits in diff views
|
||||
*/
|
||||
export const RedoIcon: React.FC<IconProps> = ({
|
||||
size = 16,
|
||||
className,
|
||||
...props
|
||||
}) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
width={size}
|
||||
height={size}
|
||||
className={className}
|
||||
aria-hidden="true"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M7 10.6667L3.66667 14L7 17.3333M3.66667 14H11.3333C12.4389 14 13.3333 13.1056 13.3333 12V4.66667C13.3333 3.56112 12.4389 2.66667 11.3333 2.66667H2.66667C1.56112 2.66667 0.666667 3.56112 0.666667 4.66667V8.66667"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.33333"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
/**
|
||||
* Replace all icon (16x16)
|
||||
* Used for replacing all occurrences in search/replace
|
||||
*/
|
||||
export const ReplaceAllIcon: React.FC<IconProps> = ({
|
||||
size = 16,
|
||||
className,
|
||||
...props
|
||||
}) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
width={size}
|
||||
height={size}
|
||||
className={className}
|
||||
aria-hidden="true"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M11.3333 5.33333L14 8L11.3333 10.6667M14 8H6C3.79086 8 2 9.79086 2 12M2.66667 10.6667L0 8L2.66667 5.33333M2.66667 8H10C12.2091 8 14 6.20914 14 4V4"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.33333"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
/**
|
||||
* Copy icon (16x16)
|
||||
* Used for copying content
|
||||
*/
|
||||
export const CopyIcon: React.FC<IconProps> = ({
|
||||
size = 16,
|
||||
className,
|
||||
...props
|
||||
}) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
width={size}
|
||||
height={size}
|
||||
className={className}
|
||||
aria-hidden="true"
|
||||
{...props}
|
||||
>
|
||||
<rect
|
||||
x="4.6665"
|
||||
y="4"
|
||||
width="8"
|
||||
height="8"
|
||||
rx="1.33333"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.33333"
|
||||
/>
|
||||
<path
|
||||
d="M6 6H5.33333C4.04767 6 3 7.04767 3 8.33333V10.6667C3 11.9523 4.04767 13 5.33333 13H7.66667C8.95233 13 10 11.9523 10 10.6667V10"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.33333"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
/**
|
||||
* Paste icon (16x16)
|
||||
* Used for pasting content
|
||||
*/
|
||||
export const PasteIcon: React.FC<IconProps> = ({
|
||||
size = 16,
|
||||
className,
|
||||
...props
|
||||
}) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
width={size}
|
||||
height={size}
|
||||
className={className}
|
||||
aria-hidden="true"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M5.3335 4.66669V4.00002C5.3335 3.62305 5.48315 3.26159 5.75181 2.99293C6.02047 2.72427 6.38193 2.57467 6.7589 2.57467H9.2589C9.63587 2.57467 9.99733 2.72427 10.266 2.99293C10.5346 3.26159 10.6842 3.62305 10.6842 4.00002V4.66669M12.0176 4.66669H12.6842C13.0612 4.66669 13.4227 4.81628 13.6913 5.08494C13.96 5.3536 14.1096 5.71506 14.1096 6.09203V10.9254C14.1096 11.3023 13.96 11.6638 13.6913 11.9325C13.4227 12.2011 13.0612 12.3507 12.6842 12.3507H3.35089C2.97392 12.3507 2.61246 12.2011 2.3438 11.9325C2.07514 11.6638 1.92554 11.3023 1.92554 10.9254V6.09203C1.92554 5.71506 2.07514 5.3536 2.3438 5.08494C2.61246 4.81628 2.97392 4.66669 3.35089 4.66669H4.01756M12.0176 4.66669V7.33335C12.0176 8.06973 11.7253 8.77607 11.2093 9.29205C10.6933 9.80803 9.98698 10.0999 9.2506 10.0999H6.77573C6.03935 10.0999 5.33301 9.80803 4.81703 9.29205C4.30105 8.77607 4.00918 8.06973 4.00918 7.33335V4.66669M12.0176 4.66669H4.01756"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.33333"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
/**
|
||||
* Select all icon (16x16)
|
||||
* Used for selecting all content
|
||||
*/
|
||||
export const SelectAllIcon: React.FC<IconProps> = ({
|
||||
size = 16,
|
||||
className,
|
||||
...props
|
||||
}) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
width={size}
|
||||
height={size}
|
||||
className={className}
|
||||
aria-hidden="true"
|
||||
{...props}
|
||||
>
|
||||
<rect
|
||||
x="2.6665"
|
||||
y="2"
|
||||
width="10.6667"
|
||||
height="12"
|
||||
rx="1.33333"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.33333"
|
||||
/>
|
||||
<path
|
||||
d="M5.3335 5.33333H8.00016M5.3335 8H10.6668M5.3335 10.6667H10.6668"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.33333"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
|
@ -1,3 +1,9 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
|
||||
interface IconProps {
|
||||
|
|
@ -12,27 +18,24 @@ const Icon: React.FC<IconProps> = ({
|
|||
size = 24,
|
||||
color = 'currentColor',
|
||||
className = '',
|
||||
}) =>
|
||||
}) => (
|
||||
// This is a placeholder - in a real implementation you might use an icon library
|
||||
(
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill={color}
|
||||
className={className}
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill={color}
|
||||
className={className}
|
||||
>
|
||||
<text
|
||||
x="50%"
|
||||
y="50%"
|
||||
dominantBaseline="middle"
|
||||
textAnchor="middle"
|
||||
fontSize="10"
|
||||
>
|
||||
<text
|
||||
x="50%"
|
||||
y="50%"
|
||||
dominantBaseline="middle"
|
||||
textAnchor="middle"
|
||||
fontSize="10"
|
||||
>
|
||||
{name}
|
||||
</text>
|
||||
</svg>
|
||||
)
|
||||
;
|
||||
|
||||
{name}
|
||||
</text>
|
||||
</svg>
|
||||
);
|
||||
export default Icon;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,9 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
|
||||
interface SendIconProps {
|
||||
|
|
@ -11,20 +17,20 @@ const SendIcon: React.FC<SendIconProps> = ({
|
|||
color = 'currentColor',
|
||||
className = '',
|
||||
}) => (
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke={color}
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={className}
|
||||
>
|
||||
<line x1="22" y1="2" x2="11" y2="13"></line>
|
||||
<polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
|
||||
</svg>
|
||||
);
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke={color}
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={className}
|
||||
>
|
||||
<line x1="22" y1="2" x2="11" y2="13"></line>
|
||||
<polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default SendIcon;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,14 @@
|
|||
*/
|
||||
|
||||
export type { IconProps } from './types.js';
|
||||
export { FileIcon, FileListIcon, FolderIcon } from './FileIcons.js';
|
||||
|
||||
// File icons
|
||||
export {
|
||||
FileIcon,
|
||||
FileListIcon,
|
||||
SaveDocumentIcon,
|
||||
FolderIcon,
|
||||
} from './FileIcons.js';
|
||||
|
||||
// Navigation icons
|
||||
export {
|
||||
|
|
@ -29,6 +36,7 @@ export {
|
|||
SlashCommandIcon,
|
||||
LinkIcon,
|
||||
OpenDiffIcon,
|
||||
UndoIcon,
|
||||
} from './EditIcons.js';
|
||||
|
||||
// Status icons
|
||||
|
|
@ -1,3 +1,9 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
|
||||
interface ContainerProps {
|
||||
|
|
@ -6,7 +12,7 @@ interface ContainerProps {
|
|||
}
|
||||
|
||||
const Container: React.FC<ContainerProps> = ({ children, className = '' }) => (
|
||||
<div className={`container mx-auto px-4 ${className}`}>{children}</div>
|
||||
);
|
||||
<div className={`container mx-auto px-4 ${className}`}>{children}</div>
|
||||
);
|
||||
|
||||
export default Container;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,9 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
|
||||
const Footer: React.FC = () => <footer>Footer Component Placeholder</footer>;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,9 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
|
||||
const Header: React.FC = () => <header>Header Component Placeholder</header>;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,9 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
|
||||
const Main: React.FC = () => <main>Main Component Placeholder</main>;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,9 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
|
||||
const Sidebar: React.FC = () => <aside>Sidebar Component Placeholder</aside>;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,9 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
|
||||
interface MessageProps {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,13 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
|
||||
const MessageInput: React.FC = () => <div>MessageInput Component Placeholder</div>;
|
||||
const MessageInput: React.FC = () => (
|
||||
<div>MessageInput Component Placeholder</div>
|
||||
);
|
||||
|
||||
export default MessageInput;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,13 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
|
||||
const MessageList: React.FC = () => <div>MessageList Component Placeholder</div>;
|
||||
const MessageList: React.FC = () => (
|
||||
<div>MessageList Component Placeholder</div>
|
||||
);
|
||||
|
||||
export default MessageList;
|
||||
|
|
|
|||
|
|
@ -6,8 +6,6 @@
|
|||
|
||||
import type React from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import './WaitingMessage.css';
|
||||
import { WITTY_LOADING_PHRASES } from '../../../../constants/loadingMessages.js';
|
||||
|
||||
interface WaitingMessageProps {
|
||||
loadingMessage: string;
|
||||
|
|
@ -16,6 +14,16 @@ interface WaitingMessageProps {
|
|||
// Rotate message every few seconds while waiting
|
||||
const ROTATE_INTERVAL_MS = 3000; // rotate every 3s per request
|
||||
|
||||
// Default witty loading phrases
|
||||
const DEFAULT_LOADING_PHRASES = [
|
||||
'Processing...',
|
||||
'Working on it...',
|
||||
'Just a moment...',
|
||||
'Loading...',
|
||||
'Hold tight...',
|
||||
'Almost there...',
|
||||
];
|
||||
|
||||
export const WaitingMessage: React.FC<WaitingMessageProps> = ({
|
||||
loadingMessage,
|
||||
}) => {
|
||||
|
|
@ -27,7 +35,7 @@ export const WaitingMessage: React.FC<WaitingMessageProps> = ({
|
|||
list.push(loadingMessage);
|
||||
set.add(loadingMessage);
|
||||
}
|
||||
for (const p of WITTY_LOADING_PHRASES) {
|
||||
for (const p of DEFAULT_LOADING_PHRASES) {
|
||||
if (!set.has(p)) {
|
||||
list.push(p);
|
||||
}
|
||||
|
|
@ -1,49 +1,143 @@
|
|||
import type React from 'react';
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
interface ButtonProps {
|
||||
import type React from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
/**
|
||||
* Button variant types
|
||||
*/
|
||||
export type ButtonVariant =
|
||||
| 'primary'
|
||||
| 'secondary'
|
||||
| 'danger'
|
||||
| 'ghost'
|
||||
| 'outline';
|
||||
|
||||
/**
|
||||
* Button size types
|
||||
*/
|
||||
export type ButtonSize = 'sm' | 'md' | 'lg';
|
||||
|
||||
/**
|
||||
* Button component props interface
|
||||
*/
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
/** Button content */
|
||||
children: React.ReactNode;
|
||||
onClick?: () => void;
|
||||
variant?: 'primary' | 'secondary' | 'danger';
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
/** Visual style variant */
|
||||
variant?: ButtonVariant;
|
||||
/** Button size */
|
||||
size?: ButtonSize;
|
||||
/** Loading state - shows spinner and disables button */
|
||||
loading?: boolean;
|
||||
/** Icon to display before children */
|
||||
leftIcon?: React.ReactNode;
|
||||
/** Icon to display after children */
|
||||
rightIcon?: React.ReactNode;
|
||||
/** Full width button */
|
||||
fullWidth?: boolean;
|
||||
}
|
||||
|
||||
const Button: React.FC<ButtonProps> = ({
|
||||
children,
|
||||
onClick,
|
||||
variant = 'primary',
|
||||
size = 'md',
|
||||
disabled = false,
|
||||
className = '',
|
||||
}) => {
|
||||
const baseClasses =
|
||||
'inline-flex items-center justify-center rounded font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2';
|
||||
/**
|
||||
* Button component with multiple variants and sizes
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <Button variant="primary" size="md" onClick={handleClick}>
|
||||
* Click me
|
||||
* </Button>
|
||||
* ```
|
||||
*/
|
||||
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
(
|
||||
{
|
||||
children,
|
||||
variant = 'primary',
|
||||
size = 'md',
|
||||
disabled = false,
|
||||
loading = false,
|
||||
leftIcon,
|
||||
rightIcon,
|
||||
fullWidth = false,
|
||||
className = '',
|
||||
type = 'button',
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const isDisabled = disabled || loading;
|
||||
|
||||
const variantClasses = {
|
||||
primary: 'bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500',
|
||||
secondary:
|
||||
'bg-gray-200 text-gray-800 hover:bg-gray-300 focus:ring-gray-500',
|
||||
danger: 'bg-red-600 text-white hover:bg-red-700 focus:ring-red-500',
|
||||
};
|
||||
const baseClasses =
|
||||
'inline-flex items-center justify-center rounded font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2';
|
||||
|
||||
const sizeClasses = {
|
||||
sm: 'px-2 py-1 text-sm',
|
||||
md: 'px-4 py-2',
|
||||
lg: 'px-6 py-3 text-lg',
|
||||
};
|
||||
const variantClasses: Record<ButtonVariant, string> = {
|
||||
primary: 'bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500',
|
||||
secondary:
|
||||
'bg-gray-200 text-gray-800 hover:bg-gray-300 focus:ring-gray-500',
|
||||
danger: 'bg-red-600 text-white hover:bg-red-700 focus:ring-red-500',
|
||||
ghost:
|
||||
'bg-transparent text-gray-700 hover:bg-gray-100 focus:ring-gray-400',
|
||||
outline:
|
||||
'bg-transparent border border-gray-300 text-gray-700 hover:bg-gray-50 focus:ring-gray-400',
|
||||
};
|
||||
|
||||
const disabledClass = disabled ? 'opacity-50 cursor-not-allowed' : '';
|
||||
const sizeClasses: Record<ButtonSize, string> = {
|
||||
sm: 'px-2 py-1 text-sm gap-1',
|
||||
md: 'px-4 py-2 gap-2',
|
||||
lg: 'px-6 py-3 text-lg gap-2',
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
className={`${baseClasses} ${variantClasses[variant]} ${sizeClasses[size]} ${disabledClass} ${className}`}
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
const disabledClass = isDisabled
|
||||
? 'opacity-50 cursor-not-allowed pointer-events-none'
|
||||
: '';
|
||||
const widthClass = fullWidth ? 'w-full' : '';
|
||||
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
type={type}
|
||||
className={`${baseClasses} ${variantClasses[variant]} ${sizeClasses[size]} ${disabledClass} ${widthClass} ${className}`.trim()}
|
||||
disabled={isDisabled}
|
||||
aria-disabled={isDisabled}
|
||||
aria-busy={loading}
|
||||
{...props}
|
||||
>
|
||||
{loading && (
|
||||
<svg
|
||||
className="animate-spin h-4 w-4"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<circle
|
||||
className="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
strokeWidth="4"
|
||||
/>
|
||||
<path
|
||||
className="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
{!loading && leftIcon}
|
||||
{children}
|
||||
{!loading && rightIcon}
|
||||
</button>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Button.displayName = 'Button';
|
||||
|
||||
export default Button;
|
||||
|
|
|
|||
|
|
@ -1,25 +1,149 @@
|
|||
import type React from 'react';
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
interface InputProps {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
placeholder?: string;
|
||||
className?: string;
|
||||
import type React from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
/**
|
||||
* Input size types
|
||||
*/
|
||||
export type InputSize = 'sm' | 'md' | 'lg';
|
||||
|
||||
/**
|
||||
* Input component props interface
|
||||
*/
|
||||
export interface InputProps
|
||||
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'size'> {
|
||||
/** Input size */
|
||||
size?: InputSize;
|
||||
/** Error state */
|
||||
error?: boolean;
|
||||
/** Error message to display */
|
||||
errorMessage?: string;
|
||||
/** Label for the input */
|
||||
label?: string;
|
||||
/** Helper text below input */
|
||||
helperText?: string;
|
||||
/** Left icon/element */
|
||||
leftElement?: React.ReactNode;
|
||||
/** Right icon/element */
|
||||
rightElement?: React.ReactNode;
|
||||
/** Full width input */
|
||||
fullWidth?: boolean;
|
||||
}
|
||||
|
||||
const Input: React.FC<InputProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
placeholder,
|
||||
className = '',
|
||||
}) => (
|
||||
<input
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
placeholder={placeholder}
|
||||
className={`border rounded px-3 py-2 ${className}`}
|
||||
/>
|
||||
);
|
||||
/**
|
||||
* Input component with multiple sizes and states
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <Input
|
||||
* label="Email"
|
||||
* placeholder="Enter your email"
|
||||
* error={!!errors.email}
|
||||
* errorMessage={errors.email}
|
||||
* />
|
||||
* ```
|
||||
*/
|
||||
const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
(
|
||||
{
|
||||
size = 'md',
|
||||
error = false,
|
||||
errorMessage,
|
||||
label,
|
||||
helperText,
|
||||
leftElement,
|
||||
rightElement,
|
||||
fullWidth = false,
|
||||
className = '',
|
||||
id,
|
||||
disabled,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const inputId = id || `input-${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
const baseClasses =
|
||||
'border rounded transition-colors focus:outline-none focus:ring-2';
|
||||
|
||||
const sizeClasses: Record<InputSize, string> = {
|
||||
sm: 'px-2 py-1 text-sm',
|
||||
md: 'px-3 py-2',
|
||||
lg: 'px-4 py-3 text-lg',
|
||||
};
|
||||
|
||||
const stateClasses = error
|
||||
? 'border-red-500 focus:ring-red-500 focus:border-red-500'
|
||||
: 'border-gray-300 focus:ring-blue-500 focus:border-blue-500';
|
||||
|
||||
const disabledClasses = disabled
|
||||
? 'bg-gray-100 cursor-not-allowed opacity-60'
|
||||
: 'bg-white';
|
||||
|
||||
const widthClass = fullWidth ? 'w-full' : '';
|
||||
|
||||
const paddingClasses = [
|
||||
leftElement ? 'pl-10' : '',
|
||||
rightElement ? 'pr-10' : '',
|
||||
].join(' ');
|
||||
|
||||
return (
|
||||
<div className={`${fullWidth ? 'w-full' : 'inline-block'}`}>
|
||||
{label && (
|
||||
<label
|
||||
htmlFor={inputId}
|
||||
className="block text-sm font-medium text-gray-700 mb-1"
|
||||
>
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
<div className="relative">
|
||||
{leftElement && (
|
||||
<div className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500">
|
||||
{leftElement}
|
||||
</div>
|
||||
)}
|
||||
<input
|
||||
ref={ref}
|
||||
id={inputId}
|
||||
disabled={disabled}
|
||||
aria-invalid={error}
|
||||
aria-describedby={
|
||||
errorMessage
|
||||
? `${inputId}-error`
|
||||
: helperText
|
||||
? `${inputId}-helper`
|
||||
: undefined
|
||||
}
|
||||
className={`${baseClasses} ${sizeClasses[size]} ${stateClasses} ${disabledClasses} ${widthClass} ${paddingClasses} ${className}`.trim()}
|
||||
{...props}
|
||||
/>
|
||||
{rightElement && (
|
||||
<div className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-500">
|
||||
{rightElement}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{errorMessage && error && (
|
||||
<p id={`${inputId}-error`} className="mt-1 text-sm text-red-600">
|
||||
{errorMessage}
|
||||
</p>
|
||||
)}
|
||||
{helperText && !error && (
|
||||
<p id={`${inputId}-helper`} className="mt-1 text-sm text-gray-500">
|
||||
{helperText}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Input.displayName = 'Input';
|
||||
|
||||
export default Input;
|
||||
|
|
|
|||
|
|
@ -1,93 +1,73 @@
|
|||
import React, { useState } from 'react';
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
interface ChildProps {
|
||||
onMouseEnter?: () => void;
|
||||
onMouseLeave?: () => void;
|
||||
onFocus?: () => void;
|
||||
onBlur?: () => void;
|
||||
tabIndex?: number;
|
||||
import type React from 'react';
|
||||
|
||||
/**
|
||||
* Tooltip component props
|
||||
*/
|
||||
export interface TooltipProps {
|
||||
/** Content to wrap with tooltip */
|
||||
children: React.ReactNode;
|
||||
/** Tooltip content (can be string or ReactNode) */
|
||||
content: React.ReactNode;
|
||||
/** Tooltip position relative to children */
|
||||
position?: 'top' | 'bottom' | 'left' | 'right';
|
||||
}
|
||||
|
||||
interface TooltipProps {
|
||||
children: React.ReactElement<ChildProps>;
|
||||
content: string;
|
||||
position?: 'top' | 'right' | 'bottom' | 'left';
|
||||
}
|
||||
|
||||
const Tooltip: React.FC<TooltipProps> = ({
|
||||
/**
|
||||
* Tooltip component using CSS group-hover for display
|
||||
* Supports CSS variables for theming
|
||||
*/
|
||||
export const Tooltip: React.FC<TooltipProps> = ({
|
||||
children,
|
||||
content,
|
||||
position = 'top',
|
||||
}) => {
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
|
||||
const positionClasses = {
|
||||
top: 'bottom-full left-1/2 transform -translate-x-1/2 mb-2',
|
||||
right: 'top-1/2 left-full transform -translate-y-1/2 ml-2',
|
||||
bottom: 'top-full left-1/2 transform -translate-x-1/2 mt-2',
|
||||
left: 'top-1/2 right-full transform -translate-y-1/2 mr-2',
|
||||
};
|
||||
|
||||
const arrowPositionClasses = {
|
||||
top: 'top-full left-1/2 transform -translate-x-1/2 -mt-1',
|
||||
right: 'top-1/2 left-0 transform -translate-y-1/2 -ml-1',
|
||||
bottom: 'top-0 left-1/2 transform -translate-x-1/2 -mb-1',
|
||||
left: 'top-1/2 right-0 transform -translate-y-1/2 -mr-1',
|
||||
};
|
||||
|
||||
const tooltipClass = `absolute ${positionClasses[position]} bg-gray-800 text-white text-xs rounded py-1 px-2 pointer-events-none z-10`;
|
||||
const arrowClass = `absolute w-2 h-2 bg-gray-800 transform rotate-45 ${arrowPositionClasses[position]}`;
|
||||
|
||||
return (
|
||||
<div className="relative inline-block">
|
||||
}) => (
|
||||
<div className="relative inline-block">
|
||||
<div className="group relative">
|
||||
{children}
|
||||
<div
|
||||
onMouseEnter={() => setIsVisible(true)}
|
||||
onMouseLeave={() => setIsVisible(false)}
|
||||
onFocus={() => setIsVisible(true)}
|
||||
onBlur={() => setIsVisible(false)}
|
||||
tabIndex={0}
|
||||
className={`
|
||||
absolute z-50 px-2 py-1 text-xs rounded-md shadow-lg
|
||||
bg-[var(--app-primary-background,#1f2937)] border border-[var(--app-input-border,#374151)]
|
||||
text-[var(--app-primary-foreground,#f9fafb)] whitespace-nowrap
|
||||
opacity-0 group-hover:opacity-100 transition-opacity duration-150
|
||||
-translate-x-1/2 left-1/2
|
||||
${
|
||||
position === 'top'
|
||||
? '-translate-y-1 bottom-full mb-1'
|
||||
: position === 'bottom'
|
||||
? 'translate-y-1 top-full mt-1'
|
||||
: position === 'left'
|
||||
? '-translate-x-full left-0 translate-y-[-50%] top-1/2'
|
||||
: 'translate-x-0 right-0 translate-y-[-50%] top-1/2'
|
||||
}
|
||||
pointer-events-none
|
||||
`}
|
||||
>
|
||||
{React.cloneElement(children, {
|
||||
onMouseEnter: () => {
|
||||
setIsVisible(true);
|
||||
const typedChildren = children as React.ReactElement<ChildProps>;
|
||||
if (typeof typedChildren.props.onMouseEnter === 'function') {
|
||||
typedChildren.props.onMouseEnter();
|
||||
{content}
|
||||
<div
|
||||
className={`
|
||||
absolute w-2 h-2 bg-[var(--app-primary-background,#1f2937)] border-l border-b border-[var(--app-input-border,#374151)]
|
||||
-rotate-45
|
||||
${
|
||||
position === 'top'
|
||||
? 'top-full left-1/2 -translate-x-1/2 -translate-y-1/2'
|
||||
: position === 'bottom'
|
||||
? 'bottom-full left-1/2 -translate-x-1/2 translate-y-1/2'
|
||||
: position === 'left'
|
||||
? 'right-full top-1/2 translate-x-1/2 -translate-y-1/2'
|
||||
: 'left-full top-1/2 -translate-x-1/2 -translate-y-1/2'
|
||||
}
|
||||
},
|
||||
onMouseLeave: () => {
|
||||
setIsVisible(false);
|
||||
const typedChildren = children as React.ReactElement<ChildProps>;
|
||||
if (typeof typedChildren.props.onMouseLeave === 'function') {
|
||||
typedChildren.props.onMouseLeave();
|
||||
}
|
||||
},
|
||||
onFocus: () => {
|
||||
setIsVisible(true);
|
||||
const typedChildren = children as React.ReactElement<ChildProps>;
|
||||
if (typeof typedChildren.props.onFocus === 'function') {
|
||||
typedChildren.props.onFocus();
|
||||
}
|
||||
},
|
||||
onBlur: () => {
|
||||
setIsVisible(false);
|
||||
const typedChildren = children as React.ReactElement<ChildProps>;
|
||||
if (typeof typedChildren.props.onBlur === 'function') {
|
||||
typedChildren.props.onBlur();
|
||||
}
|
||||
},
|
||||
tabIndex:
|
||||
(children as React.ReactElement<ChildProps>).props.tabIndex || 0,
|
||||
})}
|
||||
`}
|
||||
/>
|
||||
</div>
|
||||
{isVisible && (
|
||||
<div className={tooltipClass}>
|
||||
{content}
|
||||
<div className={arrowClass}></div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
</div>
|
||||
);
|
||||
|
||||
export default Tooltip;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,9 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
export const useLocalStorage = <T>(key: string, initialValue: T) => {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,9 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
export const useTheme = () => {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,9 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
// Shared UI Components Export
|
||||
// Export all shared components from this package
|
||||
|
||||
|
|
@ -24,11 +30,14 @@ export { default as Footer } from './components/layout/Footer';
|
|||
export { default as Message } from './components/messages/Message';
|
||||
export { default as MessageInput } from './components/messages/MessageInput';
|
||||
export { default as MessageList } from './components/messages/MessageList';
|
||||
export { WaitingMessage } from './components/messages/Waiting/WaitingMessage';
|
||||
export { InterruptedMessage } from './components/messages/Waiting/InterruptedMessage';
|
||||
|
||||
// UI Elements
|
||||
export { default as Button } from './components/ui/Button';
|
||||
export { default as Input } from './components/ui/Input';
|
||||
export { default as Tooltip } from './components/ui/Tooltip';
|
||||
export { Tooltip } from './components/ui/Tooltip';
|
||||
export type { TooltipProps } from './components/ui/Tooltip';
|
||||
|
||||
// Permission components
|
||||
export { default as PermissionDrawer } from './components/PermissionDrawer';
|
||||
|
|
@ -38,6 +47,56 @@ export { default as Icon } from './components/icons/Icon';
|
|||
export { default as CloseIcon } from './components/icons/CloseIcon';
|
||||
export { default as SendIcon } from './components/icons/SendIcon';
|
||||
|
||||
// File Icons
|
||||
export {
|
||||
FileIcon,
|
||||
FileListIcon,
|
||||
SaveDocumentIcon,
|
||||
FolderIcon,
|
||||
} from './components/icons/FileIcons';
|
||||
|
||||
// Status Icons
|
||||
export {
|
||||
PlanCompletedIcon,
|
||||
PlanInProgressIcon,
|
||||
PlanPendingIcon,
|
||||
WarningTriangleIcon,
|
||||
UserIcon,
|
||||
SymbolIcon,
|
||||
SelectionIcon,
|
||||
} from './components/icons/StatusIcons';
|
||||
|
||||
// Navigation Icons
|
||||
export {
|
||||
ChevronDownIcon,
|
||||
PlusIcon,
|
||||
PlusSmallIcon,
|
||||
ArrowUpIcon,
|
||||
CloseIcon as CloseXIcon,
|
||||
CloseSmallIcon,
|
||||
SearchIcon,
|
||||
RefreshIcon,
|
||||
} from './components/icons/NavigationIcons';
|
||||
|
||||
// Edit Icons
|
||||
export {
|
||||
EditPencilIcon,
|
||||
AutoEditIcon,
|
||||
PlanModeIcon,
|
||||
CodeBracketsIcon,
|
||||
HideContextIcon,
|
||||
SlashCommandIcon,
|
||||
LinkIcon,
|
||||
OpenDiffIcon,
|
||||
UndoIcon,
|
||||
} from './components/icons/EditIcons';
|
||||
|
||||
// Special Icons
|
||||
export { ThinkingIcon, TerminalIcon } from './components/icons/SpecialIcons';
|
||||
|
||||
// Action Icons
|
||||
export { StopIcon } from './components/icons/StopIcon';
|
||||
|
||||
// Hooks
|
||||
export { useTheme } from './hooks/useTheme';
|
||||
export { useLocalStorage } from './hooks/useLocalStorage';
|
||||
|
|
|
|||
|
|
@ -1,3 +1,9 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
export interface MessageProps {
|
||||
id: string;
|
||||
content: string;
|
||||
|
|
|
|||
|
|
@ -1 +1,7 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
export type Theme = 'light' | 'dark' | 'auto';
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue