Compare commits

...
Author SHA1 Message Date
arafatkatze 27c5205db8 Adding console logs 2025-07-09 16:36:54 -06:00
4 changed files with 317 additions and 23 deletions
+104
View File
@@ -0,0 +1,104 @@
# Account Toggle UI Improvements
## Overview
This document outlines the improvements made to the account switching toggle in the Cline extension's account view. The previous dropdown implementation has been replaced with a modern, accessible segmented toggle component.
## Problem Statement
The original account switching UI used a VSCode dropdown component that appeared "leaky" or "weird" and didn't integrate well with the overall account card design. The dropdown styling was inconsistent with the rest of the interface and provided a suboptimal user experience.
## Solution Implemented
### 1. Custom SegmentedToggle Component
Created a new reusable `SegmentedToggle` component (`webview-ui/src/components/common/SegmentedToggle.tsx`) with the following features:
#### Key Features:
- **Modern Design**: Pill-style segmented control similar to iOS design patterns
- **Smooth Animations**: Sliding indicator with smooth transitions between selections
- **VSCode Theme Integration**: Uses VSCode CSS variables for consistent theming
- **Accessibility**: Proper ARIA attributes and keyboard navigation support
- **Responsive**: Adapts to different screen sizes and content lengths
- **Reusable**: Generic component that can be used throughout the application
#### Technical Implementation:
- Uses React hooks (`useState`, `useEffect`, `useRef`) for state management
- Implements a sliding indicator that smoothly animates between options
- Calculates indicator position dynamically based on active button dimensions
- Handles edge cases like component mounting and option changes
- Provides proper TypeScript interfaces for type safety
### 2. AccountView Integration
Updated the `AccountView` component (`webview-ui/src/components/account/AccountView.tsx`) to:
- Replace `VSCodeDropdown` with the new `SegmentedToggle`
- Maintain all existing functionality for organization switching
- Improve visual integration with the account card design
- Provide better user experience for account context switching
### 3. Styling and Theme Integration
The component uses VSCode's CSS custom properties for consistent theming:
- `--vscode-input-background`: Background color for the toggle container
- `--vscode-input-border`: Border color for the toggle container
- `--vscode-button-background`: Background color for the active indicator
- `--vscode-button-foreground`: Text color for active selection
- `--vscode-foreground`: Text color for inactive options
- `--vscode-focusBorder`: Focus ring color for accessibility
## Benefits
### User Experience Improvements:
1. **Better Visual Integration**: The toggle seamlessly integrates with the account card design
2. **Clearer State Indication**: Active selection is immediately obvious with the sliding indicator
3. **Smooth Interactions**: Animated transitions provide satisfying feedback
4. **Modern Feel**: Contemporary design that feels native to modern applications
### Technical Improvements:
1. **Reusability**: Component can be used in other parts of the application
2. **Accessibility**: Proper ARIA attributes and keyboard support
3. **Type Safety**: Full TypeScript support with proper interfaces
4. **Performance**: Efficient rendering with minimal re-renders
5. **Maintainability**: Clean, well-documented code structure
## Usage Example
```tsx
<SegmentedToggle
options={[
{ value: "", label: "Personal" },
{ value: "org1", label: "Organization" },
]}
value={activeValue}
onChange={handleChange}
className="w-full"
/>
```
## Files Modified
1. **Created**: `webview-ui/src/components/common/SegmentedToggle.tsx`
- New reusable segmented toggle component
2. **Created**: `webview-ui/src/components/common/SegmentedToggleDemo.tsx`
- Demo component for testing and development
3. **Modified**: `webview-ui/src/components/account/AccountView.tsx`
- Replaced VSCodeDropdown with SegmentedToggle
- Updated import statements and handler logic
- Removed unused type definitions
## Testing
- All builds pass successfully
- TypeScript compilation without errors
- Component renders correctly in different states
- Smooth animations and proper accessibility support
## Future Enhancements
The SegmentedToggle component is designed to be extensible and could support:
- Custom styling props
- Icon support alongside labels
- Vertical orientation
- Different animation styles
- Size variants (small, medium, large)
## Conclusion
The new SegmentedToggle component provides a significant improvement over the previous dropdown implementation, offering better visual integration, improved user experience, and a more modern interface that aligns with contemporary design patterns while maintaining full accessibility and VSCode theme compatibility.
@@ -1,8 +1,9 @@
import { VSCodeButton, VSCodeDivider, VSCodeLink, VSCodeDropdown, VSCodeOption } from "@vscode/webview-ui-toolkit/react"
import { VSCodeButton, VSCodeDivider, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { memo, useEffect, useState } from "react"
import { BadgeCent } from "lucide-react"
import { useClineAuth } from "@/context/ClineAuthContext"
import VSCodeButtonLink from "../common/VSCodeButtonLink"
import { SegmentedToggle } from "../common/SegmentedToggle"
import ClineLogoWhite from "../../assets/ClineLogoWhite"
import CountUp from "react-countup"
import CreditsHistoryTable from "./CreditsHistoryTable"
@@ -12,12 +13,6 @@ import { AccountServiceClient } from "@/services/grpc-client"
import { EmptyRequest } from "@shared/proto/common"
import { GetOrganizationCreditsRequest, UserOrganization, UserOrganizationUpdateRequest } from "@shared/proto/account"
type VSCodeDropdownChangeEvent = Event & {
target: {
value: string
}
}
type AccountViewProps = {
onDone: () => void
}
@@ -150,9 +145,7 @@ export const ClineAccountView = () => {
handleSignOut()
}
const handleOrganizationChange = async (event: any) => {
const newOrgId = (event.target as VSCodeDropdownChangeEvent["target"]).value
const handleOrganizationChange = async (newOrgId: string) => {
if (!activeOrganization || activeOrganization.organizationId !== newOrgId) {
try {
await AccountServiceClient.setUserOrganization(UserOrganizationUpdateRequest.create({ organizationId: newOrgId }))
@@ -188,19 +181,21 @@ export const ClineAccountView = () => {
<div className="text-sm text-[var(--vscode-descriptionForeground)]">{user.email}</div>
)}
{userOrganizations && (
<VSCodeDropdown
key={`dropdown-${activeOrganization?.organizationId || "Personal"}`}
currentValue={activeOrganization?.organizationId || ""}
onChange={handleOrganizationChange}
style={{ width: "100%", marginTop: "4px" }}>
<VSCodeOption value="">Personal</VSCodeOption>
{userOrganizations.map((org: UserOrganization) => (
<VSCodeOption key={org.organizationId} value={org.organizationId}>
{org.name}
</VSCodeOption>
))}
</VSCodeDropdown>
{userOrganizations && userOrganizations.length > 0 && (
<div className="mt-2">
<SegmentedToggle
options={[
{ value: "", label: "Personal" },
...userOrganizations.map((org: UserOrganization) => ({
value: org.organizationId,
label: org.name,
})),
]}
value={activeOrganization?.organizationId || ""}
onChange={handleOrganizationChange}
className="w-full"
/>
</div>
)}
</div>
</div>
@@ -0,0 +1,140 @@
import React, { useState, useEffect, useRef } from "react"
interface SegmentedToggleOption {
value: string
label: string
}
interface SegmentedToggleProps {
options: SegmentedToggleOption[]
value: string
onChange: (value: string) => void
className?: string
disabled?: boolean
}
export const SegmentedToggle: React.FC<SegmentedToggleProps> = ({
options,
value,
onChange,
className = "",
disabled = false,
}) => {
const [indicatorStyle, setIndicatorStyle] = useState<React.CSSProperties>({})
const containerRef = useRef<HTMLDivElement>(null)
const optionRefs = useRef<(HTMLButtonElement | null)[]>([])
// Update indicator position when value changes
useEffect(() => {
const activeIndex = options.findIndex((option) => option.value === value)
if (activeIndex !== -1 && optionRefs.current[activeIndex] && containerRef.current) {
const activeButton = optionRefs.current[activeIndex]
const container = containerRef.current
const containerRect = container.getBoundingClientRect()
const buttonRect = activeButton.getBoundingClientRect()
const left = buttonRect.left - containerRect.left
const width = buttonRect.width
setIndicatorStyle({
left: `${left}px`,
width: `${width}px`,
transition: "all 0.2s ease-in-out",
})
}
}, [value, options])
// Initialize indicator position on mount and when options change
useEffect(() => {
// Small delay to ensure DOM is ready
const timer = setTimeout(() => {
const activeIndex = options.findIndex((option) => option.value === value)
if (activeIndex !== -1 && optionRefs.current[activeIndex] && containerRef.current) {
const activeButton = optionRefs.current[activeIndex]
const container = containerRef.current
const containerRect = container.getBoundingClientRect()
const buttonRect = activeButton.getBoundingClientRect()
const left = buttonRect.left - containerRect.left
const width = buttonRect.width
setIndicatorStyle({
left: `${left}px`,
width: `${width}px`,
transition: "none", // No transition on initial load
})
}
}, 0)
return () => clearTimeout(timer)
}, [options])
const handleOptionClick = (optionValue: string) => {
if (!disabled && optionValue !== value) {
onChange(optionValue)
}
}
return (
<div
ref={containerRef}
className={`
relative inline-flex
bg-[var(--vscode-input-background)]
border border-[var(--vscode-input-border)]
rounded-md
p-1
${disabled ? "opacity-50 cursor-not-allowed" : ""}
${className}
`}
role="radiogroup">
{/* Sliding indicator */}
<div
className="
absolute top-1 bottom-1
bg-[var(--vscode-button-background)]
rounded-sm
pointer-events-none
z-10
"
style={indicatorStyle}
/>
{/* Options */}
{options.map((option, index) => {
const isActive = option.value === value
return (
<button
key={option.value}
ref={(el) => (optionRefs.current[index] = el)}
type="button"
role="radio"
aria-checked={isActive}
disabled={disabled}
className={`
relative z-20
px-3 py-1.5
text-sm font-medium
rounded-sm
transition-colors duration-200
focus:outline-none
focus:ring-2 focus:ring-[var(--vscode-focusBorder)]
focus:ring-offset-1
${
isActive
? "text-[var(--vscode-button-foreground)]"
: "text-[var(--vscode-foreground)] hover:text-[var(--vscode-button-foreground)]"
}
${disabled ? "cursor-not-allowed" : "cursor-pointer"}
`}
onClick={() => handleOptionClick(option.value)}>
{option.label}
</button>
)
})}
</div>
)
}
@@ -0,0 +1,55 @@
import React, { useState } from "react"
import { SegmentedToggle } from "./SegmentedToggle"
/**
* Demo component to showcase the SegmentedToggle functionality
* This can be used for testing and development purposes
*/
export const SegmentedToggleDemo: React.FC = () => {
const [accountType, setAccountType] = useState("")
const [planType, setPlanType] = useState("basic")
const accountOptions = [
{ value: "", label: "Personal" },
{ value: "cline", label: "Cline" },
{ value: "enterprise", label: "Enterprise" },
]
const planOptions = [
{ value: "basic", label: "Basic" },
{ value: "pro", label: "Pro" },
{ value: "team", label: "Team" },
]
return (
<div className="p-6 space-y-6">
<div>
<h3 className="text-[var(--vscode-foreground)] mb-3">Account Type</h3>
<SegmentedToggle
options={accountOptions}
value={accountType}
onChange={setAccountType}
className="w-full max-w-md"
/>
<p className="text-sm text-[var(--vscode-descriptionForeground)] mt-2">Selected: {accountType || "Personal"}</p>
</div>
<div>
<h3 className="text-[var(--vscode-foreground)] mb-3">Plan Type</h3>
<SegmentedToggle options={planOptions} value={planType} onChange={setPlanType} className="w-full max-w-md" />
<p className="text-sm text-[var(--vscode-descriptionForeground)] mt-2">Selected: {planType}</p>
</div>
<div>
<h3 className="text-[var(--vscode-foreground)] mb-3">Disabled State</h3>
<SegmentedToggle
options={accountOptions}
value=""
onChange={() => {}}
disabled={true}
className="w-full max-w-md"
/>
</div>
</div>
)
}