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:
yiliang114 2026-01-15 19:53:19 +08:00
parent af76450dee
commit 71570540cc
45 changed files with 1049 additions and 308 deletions

View file

@ -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';

View file

@ -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>
);

View file

@ -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;

View file

@ -5,7 +5,7 @@
*/
import type React from 'react';
import { Tooltip } from '../Tooltip.js';
import { Tooltip } from '@qwen-code/webui';
interface ContextUsage {
percentLeft: number;

View file

@ -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';

View file

@ -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;

View file

@ -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;
}

View file

@ -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

View file

@ -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. */

View file

@ -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';

View file

@ -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
View 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

View file

@ -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

View file

@ -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"
},

View 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."

View file

@ -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';

View file

@ -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;

View file

@ -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>
);

View file

@ -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;

View file

@ -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;

View file

@ -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

View file

@ -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;

View file

@ -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>;

View file

@ -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>;

View file

@ -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>;

View file

@ -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>;

View file

@ -1,3 +1,9 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import type React from 'react';
interface MessageProps {

View file

@ -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;

View file

@ -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;

View file

@ -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);
}

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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) => {

View file

@ -1,3 +1,9 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import { useState, useEffect } from 'react';
export const useTheme = () => {

View file

@ -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';

View file

@ -1,3 +1,9 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
export interface MessageProps {
id: string;
content: string;

View file

@ -1 +1,7 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
export type Theme = 'light' | 'dark' | 'auto';