Skip to content

Commit a8d7c2f

Browse files
yixiangxxyixiang1claude
authored
refactor(frontend): Enhance chat input and sidebar styling (#705)
* feat: Introduce team/agent selection, add new icons, and update i18n for various chat input components. * refactor(frontend): improve chat UI layout and input components Optimize chat area layout spacing and input component structure: - Adjust ChatArea spacing (pb-6 -> pb-10, marginBottom 20vh -> 12vh) - Constrain input container max-width to 820px for better readability - Simplify QuickAccessCards implementation - Refactor ChatInputCard layout structure - Update ChatInputControls button layout - Polish SendButton and TeamSelectorButton styles - Improve UnifiedRepositorySelector component Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: Update UI styling for sidebar and quick access cards to align with new theme variables * feat: Enhance sidebar UI with updated styling, layout adjustments, and improved hover effects * fix(frontend): resolve ESLint errors and align QuickAccessCards loading state - Fix ESLint no-unused-vars errors by prefixing unused variables with underscore: - TaskSidebar: totalUnreadCount, handleMarkAllAsViewed - QuickAccessCards: hideSelected - Fix QuickAccessCards alignment in loading state to match ChatInputCard - Add w-full and mx-auto to loading skeleton container Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(e2e): update chat image browser tests for new QuickAccessCards UI Adapt selectTestTeam() helper to work with the refactored QuickAccessCards component: - Remove dependency on deleted "More" button (removed in commit 0e00c84) - Add pagination support using left/right scroll arrows - Update team card selector from button to div elements - Add support for new TeamSelectorButton component (added in commit 4646cbe) - Use role="button" instead of role="option" in TeamSelectorButton popover Fixes two failing E2E tests: - should upload image via browser and verify model receives correct image_url format - should display model response after sending image Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(e2e): handle onboarding tour overlay blocking message input clicks Enhance dismissOnboardingTour() to properly handle driver.js overlay: - Press Escape multiple times to dismiss all tour steps - Verify overlay is dismissed before proceeding - Click outside overlay as fallback if still visible Add dismissOnboardingTour() call before clicking message input: - Prevents "subtree intercepts pointer events" error from driver-overlay - Use force: true as additional safety for click operations Fixes timeout errors in both image browser E2E tests caused by tour overlay blocking interactions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(e2e): add dismissOnboardingTour before all clickable element interactions Add dismissOnboardingTour() calls before clicking: - Team card in QuickAccessCards (both direct and after scrolling) - Model selector button - Use force: true for all clicks to bypass any remaining overlays This ensures the driver-overlay is dismissed before every interaction attempt, preventing "subtree intercepts pointer events" errors. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: yixiang1 <yixiang1@staff.weibo.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 36ae16d commit a8d7c2f

27 files changed

Lines changed: 818 additions & 813 deletions

frontend/e2e/tests/tasks/chat-image-browser-e2e.spec.ts

Lines changed: 81 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -239,11 +239,23 @@ test.describe('Chat Image Browser E2E with Mock Model Server', () => {
239239
await page.waitForTimeout(500)
240240
console.log('Clicked close/skip button')
241241
} else {
242-
// Press Escape to dismiss
242+
// Press Escape multiple times to dismiss all tour steps
243+
console.log('Pressing Escape to dismiss overlay...')
244+
await page.keyboard.press('Escape')
245+
await page.waitForTimeout(300)
246+
// Press Escape again to ensure all steps are dismissed
243247
await page.keyboard.press('Escape')
244248
await page.waitForTimeout(500)
245249
console.log('Pressed Escape to dismiss overlay')
246250
}
251+
252+
// Verify overlay is gone
253+
if (await driverOverlay.isVisible({ timeout: 500 }).catch(() => false)) {
254+
console.warn('Overlay still visible, trying to click outside')
255+
// Click outside the overlay to dismiss it
256+
await page.mouse.click(10, 10)
257+
await page.waitForTimeout(500)
258+
}
247259
}
248260
} catch (_error) {
249261
console.log('No onboarding tour found or already dismissed')
@@ -252,6 +264,7 @@ test.describe('Chat Image Browser E2E with Mock Model Server', () => {
252264

253265
/**
254266
* Helper function to select the test team in the UI
267+
* Updated to work with the new QuickAccessCards pagination design (removed "More" button)
255268
*/
256269
async function selectTestTeam(page: Page): Promise<boolean> {
257270
try {
@@ -267,39 +280,69 @@ test.describe('Chat Image Browser E2E with Mock Model Server', () => {
267280
console.log('Saved initial page screenshot')
268281

269282
// Strategy 1: Look for team card directly in QuickAccessCards
270-
const teamCardButton = page.locator(`button:has-text("${TEST_TEAM_NAME}")`).first()
271-
if (await teamCardButton.isVisible({ timeout: 3000 }).catch(() => false)) {
272-
console.log('Found team card button directly, clicking...')
273-
await teamCardButton.click()
274-
await page.waitForTimeout(1000)
275-
return true
276-
}
283+
// Note: Team cards are now div elements, not buttons
284+
const quickAccessCards = page.locator('[data-tour="quick-access-cards"]')
285+
if (await quickAccessCards.isVisible({ timeout: 3000 }).catch(() => false)) {
286+
console.log('Found QuickAccessCards container')
287+
288+
// Try to find the team card by text (cards are divs with team.name)
289+
const teamCard = quickAccessCards.locator(`div:has-text("${TEST_TEAM_NAME}")`).first()
290+
if (await teamCard.isVisible({ timeout: 2000 }).catch(() => false)) {
291+
console.log('Found team card directly, clicking...')
292+
// Dismiss tour before clicking
293+
await dismissOnboardingTour(page)
294+
await teamCard.click({ force: true })
295+
await page.waitForTimeout(1000)
296+
return true
297+
}
277298

278-
// Strategy 2: Look for QuickAccessCards "More" button and search for team
279-
const moreButton = page.locator(
280-
'[data-tour="quick-access-cards"] button:has-text("更多"), [data-tour="quick-access-cards"] button:has-text("More")'
281-
)
282-
if (await moreButton.isVisible({ timeout: 3000 }).catch(() => false)) {
283-
console.log('Found "More" button in QuickAccessCards')
284-
// Use force click to bypass any remaining overlays
285-
await moreButton.click({ force: true })
286-
await page.waitForTimeout(500)
299+
// Strategy 1b: If not visible, try scrolling through pages using right arrow
300+
console.log('Team card not visible, trying pagination...')
301+
const rightArrow = quickAccessCards.locator('button[aria-label="Scroll right"]')
302+
let attempts = 0
303+
const maxAttempts = 5 // Maximum number of pages to scroll through
304+
305+
while (attempts < maxAttempts) {
306+
// Check if right arrow exists and is visible
307+
if (!(await rightArrow.isVisible({ timeout: 1000 }).catch(() => false))) {
308+
console.log('No more pages to scroll')
309+
break
310+
}
287311

288-
// Search for the test team
289-
const searchInput = page
290-
.locator('input[placeholder*="搜索"], input[placeholder*="search" i]')
291-
.first()
292-
if (await searchInput.isVisible({ timeout: 2000 }).catch(() => false)) {
293-
await searchInput.fill(TEST_TEAM_NAME)
312+
console.log(`Scrolling to next page (attempt ${attempts + 1})...`)
313+
await rightArrow.click()
294314
await page.waitForTimeout(500)
315+
316+
// Check if team card is now visible
317+
if (await teamCard.isVisible({ timeout: 1000 }).catch(() => false)) {
318+
console.log('Found team card after scrolling, clicking...')
319+
// Dismiss tour before clicking
320+
await dismissOnboardingTour(page)
321+
await teamCard.click({ force: true })
322+
await page.waitForTimeout(1000)
323+
return true
324+
}
325+
326+
attempts++
295327
}
328+
}
296329

297-
// Click on the team in the dropdown
298-
const teamOption = page.locator(`[role="option"]:has-text("${TEST_TEAM_NAME}")`).first()
330+
// Strategy 2: Try TeamSelectorButton in ChatInputControls (for new chat sessions)
331+
// This button shows "智能体" or "Agent" with AgentIcon
332+
const teamSelectorButton = page.locator(
333+
'button:has-text("智能体"), button:has-text("Agent")'
334+
).first()
335+
if (await teamSelectorButton.isVisible({ timeout: 2000 }).catch(() => false)) {
336+
console.log('Found TeamSelectorButton, clicking...')
337+
await teamSelectorButton.click()
338+
await page.waitForTimeout(500)
339+
340+
// Look for the test team in the popover (uses role="button" instead of role="option")
341+
const teamOption = page.locator(`[role="button"]:has-text("${TEST_TEAM_NAME}")`).first()
299342
if (await teamOption.isVisible({ timeout: 3000 }).catch(() => false)) {
343+
console.log('Found team in TeamSelectorButton popover, selecting...')
300344
await teamOption.click()
301345
await page.waitForTimeout(1000)
302-
console.log(`Selected team from More dropdown: ${TEST_TEAM_NAME}`)
303346
return true
304347
}
305348
}
@@ -321,7 +364,7 @@ test.describe('Chat Image Browser E2E with Mock Model Server', () => {
321364
}
322365
}
323366

324-
// Strategy 4: Direct click on team card if visible
367+
// Strategy 4: Direct click on team card if visible anywhere on page
325368
const teamCard = page.locator(`text="${TEST_TEAM_NAME}"`).first()
326369
if (await teamCard.isVisible({ timeout: 3000 }).catch(() => false)) {
327370
await teamCard.click()
@@ -361,7 +404,9 @@ test.describe('Chat Image Browser E2E with Mock Model Server', () => {
361404
// Check if model selection is required
362405
if (buttonText?.includes('Please select') || buttonText?.includes('请选择模型')) {
363406
console.log('Model selection required, clicking selector...')
364-
await modelSelectorButton.click()
407+
// Dismiss tour before clicking
408+
await dismissOnboardingTour(page)
409+
await modelSelectorButton.click({ force: true })
365410
await page.waitForTimeout(500)
366411

367412
// Look for our test model in the dropdown
@@ -567,8 +612,11 @@ test.describe('Chat Image Browser E2E with Mock Model Server', () => {
567612
return
568613
}
569614

615+
// Dismiss any onboarding tour overlay before clicking input
616+
await dismissOnboardingTour(page)
617+
570618
// For contentEditable elements, we need to click first, then type
571-
await messageInput.click()
619+
await messageInput.click({ force: true })
572620
await page.keyboard.type('What is in this image?')
573621

574622
// Step 5: Send message
@@ -707,8 +755,11 @@ test.describe('Chat Image Browser E2E with Mock Model Server', () => {
707755
return
708756
}
709757

758+
// Dismiss any onboarding tour overlay before clicking input
759+
await dismissOnboardingTour(page)
760+
710761
// For contentEditable elements, we need to click first, then type
711-
await messageInput.click()
762+
await messageInput.click({ force: true })
712763
await page.keyboard.type('Describe this image')
713764

714765
// Look for send button

frontend/src/app/globals.css

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,43 +10,46 @@
1010

1111
:root {
1212
color-scheme: light;
13-
/* ChatGPT Light Theme */
13+
/* Wegent Light Theme - Purple Primary */
1414
--color-bg-base: 255 255 255;
15-
--color-bg-surface: 249 249 249;
15+
--color-bg-surface: 255 255 255;
1616
--color-bg-muted: 243 244 246;
17-
--color-bg-hover: 229 231 235;
18-
--color-border: 224 224 224;
19-
--color-border-strong: 192 192 192;
20-
--color-text-primary: 26 26 26;
21-
--color-text-secondary: 102 102 102;
22-
--color-text-muted: 160 160 160;
17+
--color-bg-hover: 93 94 201 / 0.06;
18+
--color-border: 228 228 228;
19+
--color-border-strong: 200 200 200;
20+
--color-border-light: 243 244 246;
21+
--color-text-primary: 51 51 51;
22+
--color-text-secondary: 99 99 99;
23+
--color-text-muted: 147 147 147;
2324
--color-text-inverted: 255 255 255;
2425
--color-primary: 93 94 201;
2526
--color-primary-contrast: 255 255 255;
2627
--color-focus-ring: 93 94 201;
27-
--color-scrollbar-track: 224 224 224;
28-
--color-scrollbar-thumb: 192 192 192;
28+
--color-scrollbar-track: 228 228 228;
29+
--color-scrollbar-thumb: 200 200 200;
2930
--color-success: 34 197 94;
3031
--color-error: 239 68 68;
3132
--color-link: 93 94 201;
32-
--color-code-bg: 246 248 250;
33+
--color-code-bg: 243 244 246;
3334
--color-popover: 255 255 255;
34-
--color-popover-foreground: 26 26 26;
35-
--color-tooltip: 26 26 26;
35+
--color-popover-foreground: 51 51 51;
36+
--color-tooltip: 51 51 51;
3637
--color-tooltip-foreground: 255 255 255;
37-
--shadow-popover: 0 12px 32px rgba(15, 23, 42, 0.12);
38+
--shadow-popover: 0 12px 32px rgba(93, 94, 201, 0.12);
39+
--shadow-sidebar: 0 4px 30px rgba(93, 94, 201, 0.1);
3840
--radius: 0.5rem;
3941
}
4042

4143
[data-theme='dark'] {
4244
color-scheme: dark;
43-
/* ChatGPT Dark Theme */
45+
/* Wegent Dark Theme - Purple Primary */
4446
--color-bg-base: 14 15 15;
4547
--color-bg-surface: 26 28 28;
4648
--color-bg-muted: 33 36 36;
47-
--color-bg-hover: 42 45 45;
49+
--color-bg-hover: 118 119 218 / 0.1;
4850
--color-border: 42 45 45;
4951
--color-border-strong: 52 53 53;
52+
--color-border-light: 42 45 45;
5053
--color-text-primary: 236 236 236;
5154
--color-text-secondary: 212 212 212;
5255
--color-text-muted: 160 160 160;
@@ -65,6 +68,7 @@
6568
--color-tooltip: 236 236 236;
6669
--color-tooltip-foreground: 14 15 15;
6770
--shadow-popover: 0 8px 24px rgba(0, 0, 0, 0.5);
71+
--shadow-sidebar: 0 4px 30px rgba(0, 0, 0, 0.3);
6872
--radius: 0.5rem;
6973
}
7074

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
2+
//
3+
// SPDX-License-Identifier: Apache-2.0
4+
5+
/**
6+
* AgentIcon Component
7+
*
8+
* Custom SVG icon for agent/team representation.
9+
* Features multiple people silhouettes representing a team.
10+
*/
11+
12+
import React from 'react'
13+
14+
interface AgentIconProps {
15+
className?: string
16+
}
17+
18+
export function AgentIcon({ className }: AgentIconProps) {
19+
return (
20+
<svg
21+
width="16"
22+
height="16"
23+
viewBox="0 0 16 16"
24+
fill="none"
25+
xmlns="http://www.w3.org/2000/svg"
26+
className={className}
27+
>
28+
<g clipPath="url(#clip0_1277_3534)">
29+
<path
30+
d="M8.0311 8.04319C6.34454 8.04319 4.9705 6.67084 4.9705 4.98259C4.9705 3.29604 6.34285 1.922 8.0311 1.922C9.71935 1.922 11.0917 3.29434 11.0917 4.98259C11.09 6.67084 9.71765 8.04319 8.0311 8.04319ZM8.0311 3.25018C7.07487 3.25018 6.29869 4.02807 6.29869 4.98259C6.29869 5.93882 7.07657 6.715 8.0311 6.715C8.98732 6.715 9.76351 5.93712 9.76351 4.98259C9.76351 4.02807 8.98562 3.25018 8.0311 3.25018ZM11.1189 14.0183H4.94163C4.40153 14.0183 3.8886 13.7839 3.53532 13.3763C3.18204 12.9687 3.02239 12.4286 3.09882 11.8936L3.35189 10.1068C3.48097 9.19473 4.27414 8.50686 5.1964 8.50686H10.8658C11.7864 8.50686 12.5795 9.19473 12.7103 10.1068L12.9634 11.8936C13.0398 12.4286 12.8802 12.9687 12.5269 13.3763C12.1719 13.7839 11.659 14.0183 11.1189 14.0183ZM4.41172 12.0804C4.38114 12.291 4.47796 12.4371 4.5374 12.5067C4.59685 12.5763 4.72933 12.6918 4.94163 12.6918H11.1189C11.3312 12.6918 11.4637 12.5763 11.5231 12.5067C11.5825 12.4371 11.6794 12.291 11.6488 12.0804L11.3957 10.2936C11.3583 10.0321 11.1308 9.83334 10.8658 9.83334H5.1947C4.92974 9.83334 4.70215 10.0304 4.66478 10.2936L4.41172 12.0804ZM12.3638 8.08904C12.0734 8.08904 11.805 7.89542 11.7252 7.60159C11.6284 7.24831 11.8356 6.88315 12.1889 6.78634C12.7426 6.63348 13.1281 6.12564 13.1281 5.54987C13.1281 4.94353 12.6984 4.41531 12.104 4.29472C11.7456 4.22169 11.5129 3.87181 11.5859 3.51174C11.659 3.15337 12.0089 2.92068 12.3689 2.99371C13.5765 3.23999 14.4546 4.3151 14.4546 5.54987C14.4546 6.7201 13.6665 7.75445 12.5405 8.06527C12.481 8.08225 12.4216 8.08904 12.3638 8.08904ZM14.3527 13.0077H13.923C13.5561 13.0077 13.2589 12.7105 13.2589 12.3436C13.2589 11.9768 13.5561 11.6796 13.923 11.6796H14.3527C14.475 11.6796 14.5514 11.6133 14.5871 11.5726C14.6211 11.5335 14.6771 11.4486 14.6601 11.3263L14.4546 9.87581C14.4325 9.72464 14.3018 9.60915 14.1472 9.60915H13.889C13.5222 9.60915 13.2249 9.31192 13.2249 8.94506C13.2249 8.5782 13.5222 8.28097 13.889 8.28097H14.1472C14.9574 8.28097 15.6537 8.88561 15.7675 9.68728L15.973 11.1377C16.0393 11.6065 15.9 12.0821 15.5892 12.4405C15.2767 12.8022 14.8266 13.0077 14.3527 13.0077Z"
31+
fill="currentColor"
32+
/>
33+
<path
34+
d="M3.61519 8.08902C3.55744 8.08902 3.498 8.08053 3.43855 8.06525C2.31079 7.75443 1.52441 6.72008 1.52441 5.54985C1.52441 4.31508 2.4008 3.23997 3.6101 2.99369C3.97017 2.92066 4.32004 3.15335 4.39308 3.51172C4.46611 3.87009 4.23512 4.22167 3.87505 4.2947C3.2823 4.41529 2.85089 4.94351 2.85089 5.54985C2.85089 6.12562 3.23814 6.63346 3.79013 6.78632C4.14341 6.88313 4.35062 7.24829 4.25381 7.60157C4.17398 7.8954 3.90732 8.08902 3.61519 8.08902ZM2.05602 13.0077H1.62631C1.15245 13.0077 0.700663 12.8022 0.389848 12.4438C0.079033 12.0855 -0.0619378 11.6099 0.00599987 11.1411L0.211512 9.69235C0.325307 8.89069 1.02167 8.28604 1.83183 8.28604H2.09169C2.45855 8.28604 2.75578 8.58327 2.75578 8.95013C2.75578 9.317 2.45855 9.61423 2.09169 9.61423H1.83183C1.67897 9.61423 1.54649 9.72802 1.52611 9.88088L1.32059 11.3297C1.30361 11.4519 1.35966 11.5369 1.39363 11.5759C1.4276 11.615 1.50403 11.6829 1.62801 11.6829H2.05602C2.42288 11.6829 2.72011 11.9802 2.72011 12.347C2.72011 12.7139 2.42288 13.0077 2.05602 13.0077Z"
35+
fill="currentColor"
36+
/>
37+
</g>
38+
<defs>
39+
<clipPath id="clip0_1277_3534">
40+
<rect width="16" height="16" fill="white" />
41+
</clipPath>
42+
</defs>
43+
</svg>
44+
)
45+
}
46+
47+
export default AgentIcon
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// SPDX-FileCopyrightText: 2025 Weibo, Inc.
2+
//
3+
// SPDX-License-Identifier: Apache-2.0
4+
5+
/**
6+
* ModelIcon Component
7+
*
8+
* Custom SVG icon for AI model selection.
9+
* Features a geometric diamond/cube design representing AI models.
10+
*/
11+
12+
import React from 'react'
13+
14+
interface ModelIconProps {
15+
className?: string
16+
}
17+
18+
export function ModelIcon({ className }: ModelIconProps) {
19+
return (
20+
<svg
21+
width="16"
22+
height="16"
23+
viewBox="0 0 16 16"
24+
fill="none"
25+
xmlns="http://www.w3.org/2000/svg"
26+
className={className}
27+
>
28+
<path
29+
d="M8 1.00092C8.01781 1.00092 8.03533 1.00573 8.05078 1.01459L14.0234 4.46381H14.0244C14.0397 4.47276 14.0526 4.48554 14.0615 4.50092C14.0704 4.51633 14.0751 4.5339 14.0752 4.5517V11.4482C14.0752 11.466 14.0704 11.4835 14.0615 11.499C14.0527 11.5143 14.0397 11.5271 14.0244 11.5361H14.0234L8.05078 14.9853C8.0353 14.9942 8.01785 14.999 8 14.999C7.98215 14.999 7.9647 14.9942 7.94922 14.9853L1.97656 11.5361H1.97559C1.96031 11.5271 1.94733 11.5143 1.93848 11.499C1.92962 11.4835 1.92482 11.466 1.9248 11.4482V4.5517L1.93848 4.49994C1.94737 4.48459 1.96026 4.47176 1.97559 4.46283L7.94922 1.01459C7.96467 1.00574 7.98219 1.00092 8 1.00092Z"
30+
stroke="currentColor"
31+
strokeWidth="1.35"
32+
/>
33+
<path
34+
d="M10.9014 6.84407C11.2546 6.64017 11.3749 6.18881 11.1717 5.8363C11.1232 5.75242 11.0587 5.6789 10.9818 5.61995C10.9049 5.561 10.8172 5.51778 10.7236 5.49274C10.63 5.46771 10.5324 5.46136 10.4364 5.47406C10.3404 5.48675 10.2478 5.51824 10.1639 5.56673L7.99906 6.81642L5.8356 5.56673C5.75168 5.51728 5.6588 5.48493 5.56232 5.47157C5.46583 5.45821 5.36766 5.46409 5.27346 5.48888C5.17926 5.51367 5.09091 5.55687 5.0135 5.61599C4.93609 5.67512 4.87115 5.74899 4.82245 5.83334C4.77374 5.91769 4.74223 6.01086 4.72973 6.10746C4.71723 6.20406 4.72399 6.30218 4.74962 6.39615C4.77525 6.49013 4.81924 6.57809 4.87905 6.65497C4.93886 6.73185 5.0133 6.79612 5.09809 6.84407L7.26224 8.09376V8.11933V10.7735C7.26224 10.9691 7.33994 11.1567 7.47825 11.295C7.61656 11.4334 7.80415 11.5111 7.99975 11.5111C8.19535 11.5111 8.38294 11.4334 8.52125 11.295C8.65956 11.1567 8.73726 10.9691 8.73726 10.7735V8.11864L8.73657 8.09376L10.9014 6.84407Z"
35+
fill="currentColor"
36+
/>
37+
</svg>
38+
)
39+
}
40+
41+
export default ModelIcon

frontend/src/components/ui/action-button.tsx

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ interface ActionButtonProps {
1212
disabled?: boolean
1313
title?: string
1414
icon: React.ReactNode
15+
/** Optional text label to display next to the icon */
16+
label?: string
1517
variant?: 'default' | 'outline' | 'loading'
1618
className?: string
1719
asChild?: boolean
@@ -66,11 +68,20 @@ export function ActionButton({
6668
disabled = false,
6769
title,
6870
icon,
71+
label,
6972
variant = 'default',
7073
className = '',
7174
}: ActionButtonProps) {
72-
// Base styles shared by all variants
73-
const baseStyles = 'h-9 w-9 rounded-full flex-shrink-0'
75+
// Determine if this is an icon-only button or has a label
76+
const hasLabel = Boolean(label)
77+
78+
// Base styles - different for icon-only vs with-label buttons
79+
// Design spec: height 36px, border-radius 24px, border 1px #E4E4E4, bg white
80+
// With label: padding 10px 12px 10px 10px, gap 4px
81+
// Icon only: 36x36 circle with centered icon
82+
const baseStyles = hasLabel
83+
? 'h-9 rounded-[24px] flex-shrink-0 pl-2.5 pr-3 py-2.5 gap-1 inline-flex items-center'
84+
: 'h-9 w-9 rounded-full flex-shrink-0'
7485

7586
if (variant === 'loading') {
7687
// Static loading state (non-clickable)
@@ -79,24 +90,27 @@ export function ActionButton({
7990
className={`relative ${baseStyles} flex items-center justify-center border border-border bg-base ${className}`}
8091
>
8192
{icon}
93+
{label && <span className="text-sm text-text-primary">{label}</span>}
8294
</div>
8395
)
8496
}
8597

8698
// Clickable button (default or outline)
8799
const buttonVariant = variant === 'outline' ? 'outline' : 'ghost'
88-
const defaultClassName = variant === 'outline' ? '' : 'border border-border'
100+
// No border for default variant, create clean flat button style
101+
const defaultClassName = variant === 'outline' ? 'border border-border' : ''
89102

90103
return (
91104
<Button
92105
variant={buttonVariant}
93-
size="icon"
106+
size={hasLabel ? 'default' : 'icon'}
94107
onClick={onClick}
95108
disabled={disabled}
96109
title={title}
97110
className={`${baseStyles} ${defaultClassName} ${className}`}
98111
>
99112
{icon}
113+
{label && <span className="text-sm text-text-primary">{label}</span>}
100114
</Button>
101115
)
102116
}

0 commit comments

Comments
 (0)