diff --git a/__tests__/components/chat/error-message-banner.test.tsx b/__tests__/components/chat/error-message-banner.test.tsx index 88bcc2704..82d73628e 100644 --- a/__tests__/components/chat/error-message-banner.test.tsx +++ b/__tests__/components/chat/error-message-banner.test.tsx @@ -34,6 +34,13 @@ describe("ErrorMessageBanner", () => { expect(onRetry).toHaveBeenCalledTimes(1); }); + it("shows a red error icon beside the message", () => { + render(); + + const icon = screen.getByTestId("error-message-banner-icon"); + expect(icon).toHaveStyle({ color: "var(--oh-status-error)" }); + }); + it("uses greyscale theme tokens instead of red error styling", () => { render(); diff --git a/__tests__/components/features/conversation-panel/conversation-panel-list-helpers.test.ts b/__tests__/components/features/conversation-panel/conversation-panel-list-helpers.test.ts index bf87a56ef..08343a115 100644 --- a/__tests__/components/features/conversation-panel/conversation-panel-list-helpers.test.ts +++ b/__tests__/components/features/conversation-panel/conversation-panel-list-helpers.test.ts @@ -1,6 +1,8 @@ import { describe, expect, it } from "vitest"; import { + getGroupConversationPreview, groupConversations, + GROUP_CONVERSATIONS_PREVIEW_LIMIT, parseConversationTimeMs, sortConversationsByField, } from "#/components/features/conversation-panel/conversation-panel-list-helpers"; @@ -212,6 +214,48 @@ describe("conversation-panel-list-helpers", () => { ]); }); + it("limits grouped folder previews to five conversations with an expand path", () => { + const conversations = Array.from({ length: 6 }, (_, index) => ({ + ...base, + id: `c-${index}`, + title: `Conversation ${index}`, + updated_at: `2024-01-0${index + 1}T00:00:00.000Z`, + })); + + const truncated = getGroupConversationPreview(conversations, { + expanded: false, + }); + expect(truncated.visibleConversations.map((c) => c.id)).toEqual([ + "c-0", + "c-1", + "c-2", + "c-3", + "c-4", + ]); + expect(truncated.isPreviewTruncated).toBe(true); + expect(truncated.isShowingAll).toBe(false); + + const expanded = getGroupConversationPreview(conversations, { + expanded: true, + }); + expect(expanded.visibleConversations).toHaveLength(6); + expect(expanded.isPreviewTruncated).toBe(true); + expect(expanded.isShowingAll).toBe(true); + + const withActiveBeyondPreview = getGroupConversationPreview(conversations, { + expanded: false, + activeConversationId: "c-5", + }); + expect(withActiveBeyondPreview.visibleConversations.map((c) => c.id)).toEqual([ + "c-0", + "c-1", + "c-2", + "c-3", + "c-5", + ]); + expect(GROUP_CONVERSATIONS_PREVIEW_LIMIT).toBe(5); + }); + it("groups local conversations by selected_workspace, collapsing per-conversation worktree paths", () => { // Two conversations launched against the same workspace but with // different per-conversation worktree dirs (the agent-server runs diff --git a/__tests__/components/features/home/home-chat-launcher.test.tsx b/__tests__/components/features/home/home-chat-launcher.test.tsx index bbeb51133..f063fa742 100644 --- a/__tests__/components/features/home/home-chat-launcher.test.tsx +++ b/__tests__/components/features/home/home-chat-launcher.test.tsx @@ -9,6 +9,16 @@ import AgentServerConversationService from "#/api/conversation-service/agent-ser const mockNavigate = vi.fn(); const mockUseActiveBackend = vi.fn(); +const mockDisplayErrorToast = vi.fn(); + +vi.mock("#/utils/custom-toast-handlers", async (importOriginal) => { + const actual = + await importOriginal(); + return { + ...actual, + displayErrorToast: (...args: unknown[]) => mockDisplayErrorToast(...args), + }; +}); vi.mock("react-i18next", () => ({ useTranslation: () => ({ t: (key: string) => key }), @@ -294,7 +304,6 @@ describe("HomeChatLauncher", () => { }); it("surfaces a toast and skips navigation when conversation creation fails", async () => { - const toastErrorSpy = vi.spyOn(toast, "error"); vi.spyOn(AgentServerConversationService, "createConversation").mockRejectedValue( new Error("Network down"), ); @@ -303,7 +312,7 @@ describe("HomeChatLauncher", () => { const user = userEvent.setup(); await user.click(screen.getByTestId("stub-chat-submit")); - await waitFor(() => expect(toastErrorSpy).toHaveBeenCalled()); + await waitFor(() => expect(mockDisplayErrorToast).toHaveBeenCalled()); expect(mockNavigate).not.toHaveBeenCalled(); }); }); diff --git a/__tests__/components/settings/llm-profiles/profile-actions-menu.test.tsx b/__tests__/components/settings/llm-profiles/profile-actions-menu.test.tsx index 59ba44f78..453d7320f 100644 --- a/__tests__/components/settings/llm-profiles/profile-actions-menu.test.tsx +++ b/__tests__/components/settings/llm-profiles/profile-actions-menu.test.tsx @@ -173,11 +173,12 @@ describe("ProfileActionsMenu", () => { expect(menuItems).toHaveLength(4); }); - it("marks the Delete menu item as destructive", () => { + it("styles Delete like other menu items", () => { render(); const deleteButton = screen.getByTestId("profile-delete"); - expect(deleteButton).toHaveAttribute("data-destructive", "true"); + expect(deleteButton).not.toHaveAttribute("data-destructive"); + expect(deleteButton.className).not.toMatch(/text-red/); }); it("does not call onClose when clicking inside the menu container", () => { diff --git a/__tests__/themes/color-themes.test.tsx b/__tests__/themes/color-themes.test.tsx new file mode 100644 index 000000000..854197d4a --- /dev/null +++ b/__tests__/themes/color-themes.test.tsx @@ -0,0 +1,70 @@ +import { render, screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; +import { AgentServerUIRoot } from "#/components/providers/agent-server-ui-root"; +import { + AVAILABLE_COLOR_THEMES, + COLOR_THEMES, + applyColorTheme, +} from "#/themes/color-themes"; + +describe("color themes", () => { + it("includes OpenHands-Neo as a neutral-based theme with white button tokens", () => { + const neo = COLOR_THEMES["openhands-neo"]; + + expect(neo.label).toBe("OpenHands-Neo"); + expect(neo.scale).toEqual(COLOR_THEMES["openhands-neutral"].scale); + expect(neo.heroui).toEqual(COLOR_THEMES["openhands-neutral"].heroui); + expect(neo.tokens?.["--oh-color-primary"]).toBe("#ffffff"); + expect(neo.tokens?.["--oh-accent"]).toBe("#ffffff"); + }); + + it("exposes Neo in the settings theme picker", () => { + expect(AVAILABLE_COLOR_THEMES.map((theme) => theme.key)).toContain( + "openhands-neo", + ); + expect( + AVAILABLE_COLOR_THEMES.find((theme) => theme.key === "openhands-neo") + ?.label, + ).toBe("OpenHands-Neo"); + }); + + it("injects white primary tokens when applying OpenHands-Neo", () => { + document.body.setAttribute("data-agent-server-ui", ""); + + applyColorTheme("openhands-neo"); + + const styleEl = document.getElementById("oh-color-theme-override"); + expect(styleEl?.textContent).toContain("--oh-color-primary: #ffffff;"); + expect(styleEl?.textContent).toContain("--oh-accent: #ffffff;"); + + styleEl?.remove(); + document.body.removeAttribute("data-agent-server-ui"); + document.body.style.removeProperty("--oh-color-primary"); + document.body.style.removeProperty("--oh-accent"); + document.body.style.removeProperty("--oh-warning"); + }); + + it("applies Neo button tokens on the scoped UI root used by primary buttons", () => { + render( + + + , + ); + + applyColorTheme("openhands-neo"); + + const scopeRoot = screen.getByTestId("primary-button").closest( + "[data-agent-server-ui]", + ) as HTMLElement; + + expect(scopeRoot.style.getPropertyValue("--oh-color-primary")).toBe( + "#ffffff", + ); + + applyColorTheme("openhands-neutral"); + + expect(scopeRoot.style.getPropertyValue("--oh-color-primary")).toBe(""); + }); +}); diff --git a/__tests__/utils/custom-toast-handlers.test.ts b/__tests__/utils/custom-toast-handlers.test.ts index 610b23890..251c01bd6 100644 --- a/__tests__/utils/custom-toast-handlers.test.ts +++ b/__tests__/utils/custom-toast-handlers.test.ts @@ -5,12 +5,18 @@ import { displayErrorToast, } from "#/utils/custom-toast-handlers"; -// Mock react-hot-toast -vi.mock("react-hot-toast", () => ({ - default: { +const { toastMock } = vi.hoisted(() => ({ + toastMock: Object.assign(vi.fn(), { success: vi.fn(), error: vi.fn(), - }, + loading: vi.fn(), + dismiss: vi.fn(), + }), +})); + +// Mock react-hot-toast +vi.mock("react-hot-toast", () => ({ + default: toastMock, })); describe("custom-toast-handlers", () => { @@ -29,6 +35,7 @@ describe("custom-toast-handlers", () => { duration: 5000, // Should use minimum duration of 5000ms position: "top-right", style: expect.objectContaining({ + borderRadius: "var(--oh-radius)", maxWidth: "400px", wordBreak: "break-word", }), @@ -47,6 +54,7 @@ describe("custom-toast-handlers", () => { duration: expect.any(Number), position: "top-right", style: expect.objectContaining({ + borderRadius: "var(--oh-radius)", maxWidth: "400px", wordBreak: "break-word", }), @@ -67,44 +75,48 @@ describe("custom-toast-handlers", () => { }); describe("displayErrorToast", () => { - it("should call toast.error with calculated duration for short message", () => { + it("should call toast with calculated duration for short message", () => { const shortMessage = "Error occurred"; displayErrorToast(shortMessage); - expect(toast.error).toHaveBeenCalledWith( + expect(toastMock).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ duration: 4000, // Should use minimum duration of 4000ms for errors position: "top-right", + icon: null, style: expect.objectContaining({ + borderRadius: "var(--oh-radius)", maxWidth: "400px", wordBreak: "break-word", + color: "var(--oh-muted)", }), }), ); }); - it("should call toast.error with longer duration for long error message", () => { + it("should call toast with longer duration for long error message", () => { const longMessage = "A very long error message that should take more time to read and understand what went wrong with the operation."; displayErrorToast(longMessage); - expect(toast.error).toHaveBeenCalledWith( + expect(toastMock).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ duration: expect.any(Number), position: "top-right", + icon: null, style: expect.objectContaining({ + borderRadius: "var(--oh-radius)", maxWidth: "400px", wordBreak: "break-word", + color: "var(--oh-muted)", }), }), ); // Get the actual duration that was passed - const callArgs = ( - toast.error as unknown as { mock: { calls: unknown[][] } } - ).mock.calls[0][1] as { duration: number }; + const callArgs = toastMock.mock.calls[0][1] as { duration: number }; const actualDuration = callArgs.duration; // For a long message, duration should be more than the minimum 4000ms diff --git a/__tests__/utils/form-control-classes.test.ts b/__tests__/utils/form-control-classes.test.ts new file mode 100644 index 000000000..e52d64519 --- /dev/null +++ b/__tests__/utils/form-control-classes.test.ts @@ -0,0 +1,22 @@ +import { describe, expect, it } from "vitest"; +import { + formControlButtonClassName, + formControlFieldClassName, + formControlShellClassName, +} from "#/utils/form-control-classes"; + +describe("formControlClasses", () => { + it("standardizes fields, shells, and buttons to 36px with rounded-lg", () => { + expect(formControlFieldClassName).toContain("h-9"); + expect(formControlFieldClassName).toContain("rounded-lg"); + expect(formControlFieldClassName).toContain("border-[var(--oh-border)]"); + expect(formControlFieldClassName).toContain("bg-base-secondary"); + + expect(formControlShellClassName).toContain("h-9"); + expect(formControlShellClassName).toContain("rounded-lg"); + expect(formControlShellClassName).toContain("focus-within:ring-1"); + + expect(formControlButtonClassName).toContain("h-9"); + expect(formControlButtonClassName).toContain("rounded-lg"); + }); +}); diff --git a/src/components/conversation-events/chat/event-message-components/skill-ready-content-list.tsx b/src/components/conversation-events/chat/event-message-components/skill-ready-content-list.tsx index fd4421b4e..19520ed2f 100644 --- a/src/components/conversation-events/chat/event-message-components/skill-ready-content-list.tsx +++ b/src/components/conversation-events/chat/event-message-components/skill-ready-content-list.tsx @@ -45,7 +45,7 @@ export function SkillReadyContentList({ items }: SkillReadyContentListProps) { )} - + {item.name} diff --git a/src/components/features/automations/automation-group.tsx b/src/components/features/automations/automation-group.tsx index 225adfba4..5904965ed 100644 --- a/src/components/features/automations/automation-group.tsx +++ b/src/components/features/automations/automation-group.tsx @@ -24,7 +24,7 @@ export function AutomationGroup({ return (
-

{title}

+

{title}

diff --git a/src/components/features/automations/backend-not-configured.tsx b/src/components/features/automations/backend-not-configured.tsx index 6f2a91ddc..56a40c546 100644 --- a/src/components/features/automations/backend-not-configured.tsx +++ b/src/components/features/automations/backend-not-configured.tsx @@ -1,6 +1,7 @@ import { useTranslation } from "react-i18next"; import { I18nKey } from "#/i18n/declaration"; import ExclamationCircleIcon from "#/icons/exclamation-circle.svg?react"; +import { BrandButton } from "#/components/features/settings/brand-button"; interface BackendUnavailableProps { onRetry: () => void; @@ -12,19 +13,20 @@ export function BackendUnavailable({ onRetry }: BackendUnavailableProps) { return (
-

+

{t(I18nKey.AUTOMATIONS$BACKEND_UNAVAILABLE_TITLE)}

{t(I18nKey.AUTOMATIONS$BACKEND_UNAVAILABLE_MESSAGE)}

- +
); } diff --git a/src/components/features/automations/create-instructions.tsx b/src/components/features/automations/create-instructions.tsx index 25ca5d42e..e939188c2 100644 --- a/src/components/features/automations/create-instructions.tsx +++ b/src/components/features/automations/create-instructions.tsx @@ -65,7 +65,7 @@ export function CreateInstructions({

{t(I18nKey.AUTOMATIONS$EMPTY_START_CONVERSATION)} @@ -96,7 +96,7 @@ export function CreateInstructions({ aria-expanded={isExpanded} className="flex w-full items-center justify-between p-4 text-left hover:bg-surface-raised transition-colors rounded-lg" > - + {t(I18nKey.AUTOMATIONS$EMPTY_HOW_TO_CREATE_TITLE)} -

+

{t(I18nKey.AUTOMATIONS$DELETE_CONFIRM_TITLE)}

diff --git a/src/components/features/automations/detail/detail-header.tsx b/src/components/features/automations/detail/detail-header.tsx index 050b59cb2..f478bc364 100644 --- a/src/components/features/automations/detail/detail-header.tsx +++ b/src/components/features/automations/detail/detail-header.tsx @@ -71,7 +71,7 @@ export function DetailHeader({

-

+

{automation.name}

diff --git a/src/components/features/automations/detail/edit-automation-modal.tsx b/src/components/features/automations/detail/edit-automation-modal.tsx index 84e3b16cc..f22d4ff8d 100644 --- a/src/components/features/automations/detail/edit-automation-modal.tsx +++ b/src/components/features/automations/detail/edit-automation-modal.tsx @@ -18,6 +18,11 @@ import { parseTimeOfDay, type SchedulePresetKind, } from "#/utils/automation-schedule"; +import { cn } from "#/utils/utils"; +import { + formControlMultilineFieldClassName, + formControlSettingsFieldClassName, +} from "#/utils/form-control-classes"; import XMarkIcon from "#/icons/x-mark.svg?react"; interface EditAutomationModalProps { @@ -214,7 +219,7 @@ export function EditAutomationModal({ -

+

{t(I18nKey.AUTOMATIONS$EDIT_TITLE)}

@@ -244,7 +249,10 @@ export function EditAutomationModal({ setForm((f) => ({ ...f, prompt: e.target.value })) } rows={4} - className="bg-tertiary border border-[var(--oh-border-input)] w-full min-w-0 rounded-sm p-2 text-sm placeholder:text-tertiary-alt" + className={cn( + formControlMultilineFieldClassName, + "placeholder:italic", + )} /> {t(I18nKey.AUTOMATIONS$EDIT_PROMPT_HINT)} @@ -291,7 +299,10 @@ export function EditAutomationModal({ setForm((f) => ({ ...f, timeOfDay: e.target.value })) } disabled={form.isCustomSchedule && !isTimeEditable} - className="bg-tertiary border border-[var(--oh-border-input)] h-10 w-full min-w-0 rounded-sm p-2 disabled:bg-[var(--oh-surface-raised)] disabled:cursor-not-allowed" + className={cn( + formControlSettingsFieldClassName, + "disabled:bg-[var(--oh-surface-raised)]", + )} /> {automation.timezone && ( diff --git a/src/components/features/automations/detail/run-logs-modal.tsx b/src/components/features/automations/detail/run-logs-modal.tsx index 4c6441dd9..2ef4c94df 100644 --- a/src/components/features/automations/detail/run-logs-modal.tsx +++ b/src/components/features/automations/detail/run-logs-modal.tsx @@ -97,7 +97,7 @@ export function RunLogsModal({ const activeBody = activeTab === "stdout" ? stdout : stderr; const tabBaseClass = - "border-b-2 px-3 py-2 text-sm font-medium transition-colors focus:outline-none"; + "border-b-2 px-3 py-2 text-sm font-normal transition-colors focus:outline-none"; const tabActiveClass = "border-[var(--oh-primary)] text-white"; const tabInactiveClass = "border-transparent text-muted hover:text-content"; @@ -126,7 +126,7 @@ export function RunLogsModal({ -

+

{t(I18nKey.AUTOMATIONS$DETAIL$LOGS_TITLE)}

diff --git a/src/components/features/automations/recommended-automations-section.tsx b/src/components/features/automations/recommended-automations-section.tsx index ce27b6bc9..94baaaf13 100644 --- a/src/components/features/automations/recommended-automations-section.tsx +++ b/src/components/features/automations/recommended-automations-section.tsx @@ -96,7 +96,7 @@ export function RecommendedAutomationsSection({ return (
-

+

{t(I18nKey.RECOMMENDED_AUTOMATIONS$SECTION_TITLE)}

@@ -122,10 +122,10 @@ export function RecommendedAutomationsSection({ >
-
+
{automation.category}
-

+

{automation.name}

diff --git a/src/components/features/automations/search-input.tsx b/src/components/features/automations/search-input.tsx index aa2f0c85e..cb19dcbca 100644 --- a/src/components/features/automations/search-input.tsx +++ b/src/components/features/automations/search-input.tsx @@ -1,24 +1,31 @@ import { useTranslation } from "react-i18next"; +import { Search } from "lucide-react"; import { I18nKey } from "#/i18n/declaration"; -import SearchIcon from "#/icons/search.svg?react"; +import { cn } from "#/utils/utils"; +import { + formControlInlineInputClassName, + formControlShellClassName, +} from "#/utils/form-control-classes"; interface SearchInputProps { value: string; onChange: (value: string) => void; + className?: string; } -export function SearchInput({ value, onChange }: SearchInputProps) { +export function SearchInput({ value, onChange, className }: SearchInputProps) { const { t } = useTranslation("openhands"); return ( -

- +
+ onChange(e.target.value)} placeholder={t(I18nKey.AUTOMATIONS$SEARCH_PLACEHOLDER)} - className="w-full max-w-sm rounded-lg border border-[var(--oh-border)] bg-[var(--oh-surface)] py-2 pl-10 pr-3 text-sm text-white placeholder:text-muted focus:border-[var(--oh-border)] focus:outline-none" + aria-label={t(I18nKey.AUTOMATIONS$SEARCH_PLACEHOLDER)} + className={formControlInlineInputClassName} />
); diff --git a/src/components/features/automations/toggle-switch.tsx b/src/components/features/automations/toggle-switch.tsx index 5b2b1dabe..27a5dcf40 100644 --- a/src/components/features/automations/toggle-switch.tsx +++ b/src/components/features/automations/toggle-switch.tsx @@ -1,37 +1 @@ -import { cn } from "#/utils/utils"; - -interface ToggleSwitchProps { - enabled: boolean; - label: string; - onToggle: () => void; -} - -export function ToggleSwitch({ enabled, label, onToggle }: ToggleSwitchProps) { - return ( - - ); -} +export { ToggleSwitch } from "#/ui/toggle-switch"; diff --git a/src/components/features/backends/backend-form-modal.tsx b/src/components/features/backends/backend-form-modal.tsx index 88a5f3b27..494965230 100644 --- a/src/components/features/backends/backend-form-modal.tsx +++ b/src/components/features/backends/backend-form-modal.tsx @@ -522,7 +522,7 @@ function CloudLoginColumn({ onClose }: { onClose: () => void }) {

{t(I18nKey.BACKEND$CLOUD_TITLE)} diff --git a/src/components/features/backends/backend-selector.tsx b/src/components/features/backends/backend-selector.tsx index 092837d0f..c6b7537dc 100644 --- a/src/components/features/backends/backend-selector.tsx +++ b/src/components/features/backends/backend-selector.tsx @@ -26,6 +26,8 @@ import { useConversationStore } from "#/stores/conversation-store"; import { AddBackendModal } from "./add-backend-modal"; import { BackendStatusDot } from "./backend-status-dot"; import { ManageBackendsModal } from "./manage-backends-modal"; +import { cn } from "#/utils/utils"; +import { formControlTransitionClassName } from "#/utils/form-control-classes"; const VALUE_SEPARATOR = "::"; @@ -325,7 +327,7 @@ export function BackendSelector({ placeholder={active.backend.name} loading={someCloudLoading} options={options} - className="h-10 px-2 py-0 bg-transparent border-transparent hover:bg-[var(--oh-surface-raised)] focus-within:bg-[var(--oh-surface-raised)]" + className="h-10 px-2 py-0 bg-transparent border-transparent hover:bg-[var(--oh-surface-raised)] focus-within:bg-[var(--oh-surface-raised)] focus-within:border-transparent focus-within:ring-0" />

{!hideTrigger ? ( @@ -342,8 +344,14 @@ export function BackendSelector({ onClick={() => navigate("/settings")} className={ isSettingsActive - ? "inline-flex items-center justify-center shrink-0 w-9 h-9 rounded-md bg-tertiary text-white font-medium transition-colors cursor-pointer" - : "inline-flex items-center justify-center shrink-0 w-9 h-9 rounded-md text-[var(--oh-muted)] hover:text-white hover:bg-[var(--oh-surface-raised)] transition-colors cursor-pointer" + ? cn( + "inline-flex items-center justify-center shrink-0 w-9 h-9 rounded-md bg-tertiary text-white font-normal cursor-pointer", + formControlTransitionClassName, + ) + : cn( + "inline-flex items-center justify-center shrink-0 w-9 h-9 rounded-md text-[var(--oh-muted)] hover:text-white hover:bg-[var(--oh-surface-raised)] cursor-pointer", + formControlTransitionClassName, + ) } > diff --git a/src/components/features/backends/backend-status-dot.tsx b/src/components/features/backends/backend-status-dot.tsx index 8569132e2..3ac5910d0 100644 --- a/src/components/features/backends/backend-status-dot.tsx +++ b/src/components/features/backends/backend-status-dot.tsx @@ -21,7 +21,7 @@ export function BackendStatusDot({ let label: string; let status: string; if (isConnected === true) { - color = "bg-green-500"; + color = "bg-[var(--oh-status-success)]"; label = "Connected"; status = "connected"; } else if (isConnected === false) { diff --git a/src/components/features/backends/manage-backends-modal.tsx b/src/components/features/backends/manage-backends-modal.tsx index d2f05fb16..31a6e3538 100644 --- a/src/components/features/backends/manage-backends-modal.tsx +++ b/src/components/features/backends/manage-backends-modal.tsx @@ -159,7 +159,10 @@ export function ManageBackendsModal({ onClose }: ManageBackendsModalProps) { )} >
- + {contextMenuOpen && (
- + {t(I18nKey.LANDING$TITLE)}
diff --git a/src/components/features/chat/components/chat-input-actions.tsx b/src/components/features/chat/components/chat-input-actions.tsx index 544cb81b6..1a63ba505 100644 --- a/src/components/features/chat/components/chat-input-actions.tsx +++ b/src/components/features/chat/components/chat-input-actions.tsx @@ -38,6 +38,7 @@ import { ContextMenu } from "#/ui/context-menu"; import { Divider } from "#/ui/divider"; import { useClickOutsideElement } from "#/hooks/use-click-outside-element"; import { cn } from "#/utils/utils"; +import { formControlTransitionClassName } from "#/utils/form-control-classes"; interface ChatInputActionsProps { disabled: boolean; @@ -451,12 +452,18 @@ export function ChatInputActions({ {llmDestinationLabel} @@ -500,7 +507,8 @@ export function ChatInputActions({ ref={overflowTriggerRef} type="button" className={cn( - "flex size-6 items-center justify-center rounded-full text-[var(--oh-muted)] transition-colors", + "flex size-6 items-center justify-center rounded-full text-[var(--oh-muted)]", + formControlTransitionClassName, "hover:bg-white/10 hover:text-white cursor-pointer", )} aria-label="More input actions" diff --git a/src/components/features/chat/components/chat-input-model.tsx b/src/components/features/chat/components/chat-input-model.tsx index 04b5af5d9..6de537ee8 100644 --- a/src/components/features/chat/components/chat-input-model.tsx +++ b/src/components/features/chat/components/chat-input-model.tsx @@ -1,6 +1,6 @@ import { useActiveConversation } from "#/hooks/query/use-active-conversation"; import { useSettings } from "#/hooks/query/use-settings"; -import ChevronDownSmallIcon from "#/icons/chevron-down-small.svg?react"; +import { ComboboxCaretInline } from "#/ui/combobox-caret"; import SettingsGearIcon from "#/icons/settings-gear.svg?react"; import { useClickOutsideElement } from "#/hooks/use-click-outside-element"; import { NavigationLink } from "#/components/shared/navigation-link"; @@ -67,7 +67,7 @@ export function ChatInputModel() { {isPopoverOpen && ( diff --git a/src/components/features/chat/components/slash-command-menu.tsx b/src/components/features/chat/components/slash-command-menu.tsx index 4a8cdf0a9..98a47814b 100644 --- a/src/components/features/chat/components/slash-command-menu.tsx +++ b/src/components/features/chat/components/slash-command-menu.tsx @@ -110,7 +110,7 @@ function SlashCommandMenuItem({ onSelect(item); }} > - {item.command} + {item.command} {description && ( {description} diff --git a/src/components/features/chat/error-message-banner.tsx b/src/components/features/chat/error-message-banner.tsx index a5814911c..63dd1f158 100644 --- a/src/components/features/chat/error-message-banner.tsx +++ b/src/components/features/chat/error-message-banner.tsx @@ -1,6 +1,7 @@ import React from "react"; import { Trans, useTranslation } from "react-i18next"; -import { X } from "lucide-react"; +import { CircleX, X } from "lucide-react"; +import { OH_STATUS_ERROR_COLOR } from "#/constants/status-colors"; import { I18nKey } from "#/i18n/declaration"; import { cn } from "#/utils/utils"; @@ -63,6 +64,13 @@ export function ErrorMessageBanner({ )} data-testid="error-message-banner" > +
setIsExpanded((prev) => !prev)} data-testid="error-message-banner-toggle" > @@ -99,7 +107,7 @@ export function ErrorMessageBanner({ diff --git a/src/components/features/chat/plan-preview.tsx b/src/components/features/chat/plan-preview.tsx index 430dfa826..1dd63a7be 100644 --- a/src/components/features/chat/plan-preview.tsx +++ b/src/components/features/chat/plan-preview.tsx @@ -71,7 +71,7 @@ export function PlanPreview({ {/* Header */}
- + {t(I18nKey.COMMON$PLAN_MD)}
@@ -81,7 +81,7 @@ export function PlanPreview({ className="flex items-center gap-1 hover:opacity-80 transition-opacity cursor-pointer" data-testid="plan-preview-view-button" > - + {t(I18nKey.COMMON$VIEW)} @@ -129,9 +129,9 @@ export function PlanPreview({ )} data-testid="plan-preview-build-button" > - + {t(I18nKey.COMMON$BUILD)}{" "} - + ⌘↩ diff --git a/src/components/features/chat/switch-profile-button.tsx b/src/components/features/chat/switch-profile-button.tsx index c2f07b68f..bd2c11b78 100644 --- a/src/components/features/chat/switch-profile-button.tsx +++ b/src/components/features/chat/switch-profile-button.tsx @@ -1,7 +1,7 @@ import React from "react"; import { useTranslation } from "react-i18next"; import { I18nKey } from "#/i18n/declaration"; -import ChevronDownSmallIcon from "#/icons/chevron-down-small.svg?react"; +import { ComboboxCaretInline } from "#/ui/combobox-caret"; import { useLlmProfiles } from "#/hooks/query/use-llm-profiles"; import { useSwitchLlmProfileAndLog } from "#/hooks/mutation/use-switch-llm-profile-and-log"; import { useActiveConversation } from "#/hooks/query/use-active-conversation"; @@ -88,7 +88,7 @@ export function SwitchProfileButton() { aria-haspopup="menu" aria-expanded={contextMenuOpen} className={cn( - "inline-flex items-center gap-1 rounded-[100px] border border-transparent px-1.5 text-sm font-normal leading-5 text-[var(--oh-muted)] whitespace-nowrap min-w-0 transition-[border-color,color] max-w-[200px]", + "inline-flex items-center gap-1 rounded-[100px] border border-transparent px-1.5 text-sm font-normal leading-5 text-[var(--oh-muted)] whitespace-nowrap min-w-0 transition-[border-color,background-color,box-shadow,opacity] duration-150 motion-reduce:transition-none max-w-[200px]", "hover:text-white hover:bg-white/10 cursor-pointer", "disabled:opacity-50 disabled:cursor-not-allowed", )} @@ -96,13 +96,7 @@ export function SwitchProfileButton() { {activeProfileName ?? t(I18nKey.LLM$SELECT_MODEL_PLACEHOLDER)} - + {contextMenuOpen && ( ; @@ -17,7 +18,8 @@ export function ContextMenuIconText({
diff --git a/src/components/features/controls/server-status-context-menu-icon-text.tsx b/src/components/features/controls/server-status-context-menu-icon-text.tsx index e56d5c3fd..948811fb4 100644 --- a/src/components/features/controls/server-status-context-menu-icon-text.tsx +++ b/src/components/features/controls/server-status-context-menu-icon-text.tsx @@ -1,3 +1,7 @@ +import { ContextMenuListItem } from "#/components/features/context-menu/context-menu-list-item"; +import { formControlTransitionClassName } from "#/utils/form-control-classes"; +import { cn } from "#/utils/utils"; + interface ServerStatusContextMenuIconTextProps { icon: React.ReactNode; text: string; @@ -12,14 +16,19 @@ export function ServerStatusContextMenuIconText({ testId, }: ServerStatusContextMenuIconTextProps) { return ( - + +
+ {text} + + {icon} + +
+
); } diff --git a/src/components/features/controls/server-status-context-menu.tsx b/src/components/features/controls/server-status-context-menu.tsx index 0d88ba81e..b83380175 100644 --- a/src/components/features/controls/server-status-context-menu.tsx +++ b/src/components/features/controls/server-status-context-menu.tsx @@ -49,12 +49,12 @@ export function ServerStatusContextMenu({ {shouldActionShown && ( <> - + {isActive && onStopServer && (
{icon} @@ -31,7 +35,10 @@ export function ToolsContextMenuIconText({
{rightIcon ? ( {rightIcon} diff --git a/src/components/features/controls/tools.tsx b/src/components/features/controls/tools.tsx index f7552201b..d97658958 100644 --- a/src/components/features/controls/tools.tsx +++ b/src/components/features/controls/tools.tsx @@ -3,7 +3,7 @@ import { Wrench } from "lucide-react"; import { useTranslation } from "react-i18next"; import { I18nKey } from "#/i18n/declaration"; import { useOptionalConversationId } from "#/hooks/use-conversation-id"; -import ChevronDownSmallIcon from "#/icons/chevron-down-small.svg?react"; +import { ComboboxCaretInline } from "#/ui/combobox-caret"; import { ToolsContextMenu } from "./tools-context-menu"; import { useConversationNameContextMenu } from "#/hooks/use-conversation-name-context-menu"; import { useActiveConversation } from "#/hooks/query/use-active-conversation"; @@ -49,7 +49,7 @@ export function Tools() {
{contextMenuOpen && ( {acpDisplayName ? ( -
+
{llmModel} @@ -90,9 +97,8 @@ export function ConversationCardFooter({ ) : null}
{showRepositoryMetadata && diff --git a/src/components/features/conversation-panel/conversation-card/conversation-repo-link.tsx b/src/components/features/conversation-panel/conversation-card/conversation-repo-link.tsx index 6bed9ae39..98340a5d5 100644 --- a/src/components/features/conversation-panel/conversation-card/conversation-repo-link.tsx +++ b/src/components/features/conversation-panel/conversation-card/conversation-repo-link.tsx @@ -24,25 +24,25 @@ export function ConversationRepoLink({ : null; return ( -
-
- {Icon && } +
+
+ {Icon && } {selectedRepository.git_provider === "azure_devops" && ( - + )} {selectedRepository.selected_repository}
-
- +
+ {selectedRepository.selected_branch} diff --git a/src/components/features/conversation-panel/conversation-card/no-repository.tsx b/src/components/features/conversation-panel/conversation-card/no-repository.tsx index 6b7153b3b..5070b41fc 100644 --- a/src/components/features/conversation-panel/conversation-card/no-repository.tsx +++ b/src/components/features/conversation-panel/conversation-card/no-repository.tsx @@ -1,5 +1,4 @@ import { useTranslation } from "react-i18next"; -import { FaFolder } from "react-icons/fa6"; import { I18nKey } from "#/i18n/declaration"; import RepoForkedIcon from "#/icons/repo-forked.svg?react"; import { getPathBasename } from "#/utils/path-utils"; @@ -17,13 +16,12 @@ export function NoRepository({ workspaceWorkingDir }: NoRepositoryProps) { if (folderName) { return ( -
- - {folderName} -
+ {folderName} + ); } diff --git a/src/components/features/conversation-panel/conversation-panel-filter-menu.tsx b/src/components/features/conversation-panel/conversation-panel-filter-menu.tsx index e3a785b51..7a0c1bdb8 100644 --- a/src/components/features/conversation-panel/conversation-panel-filter-menu.tsx +++ b/src/components/features/conversation-panel/conversation-panel-filter-menu.tsx @@ -19,6 +19,7 @@ import { I18nKey } from "#/i18n/declaration"; import type { BackendKind } from "#/api/backend-registry/types"; import { Divider } from "#/ui/divider"; import { cn } from "#/utils/utils"; +import { formControlTransitionClassName } from "#/utils/form-control-classes"; import type { ConversationSortField, OrganizeMode, @@ -111,7 +112,10 @@ function MenuRow({ )} > {label} @@ -234,7 +238,10 @@ export function ConversationPanelFilterMenu({ aria-haspopup="menu" aria-expanded={filterMenuOpen} onClick={() => setFilterMenuOpen(!filterMenuOpen)} - className="inline-flex h-7 w-7 items-center justify-center rounded-md text-[var(--oh-muted)] hover:text-white hover:bg-[var(--oh-surface-raised)] transition-colors" + className={cn( + "inline-flex h-7 w-7 items-center justify-center rounded-md text-[var(--oh-muted)] hover:text-white hover:bg-[var(--oh-surface-raised)]", + formControlTransitionClassName, + )} > limit, + isShowingAll: true, + }; + } + + const activeIndex = + options.activeConversationId != null + ? conversations.findIndex((c) => c.id === options.activeConversationId) + : -1; + + if (activeIndex >= limit) { + const activeConversation = conversations[activeIndex]; + return { + visibleConversations: [ + ...conversations.slice(0, limit - 1), + activeConversation, + ], + isPreviewTruncated: true, + isShowingAll: false, + }; + } + + return { + visibleConversations: conversations.slice(0, limit), + isPreviewTruncated: conversations.length > limit, + isShowingAll: false, + }; +} + /** Subset of `useCreateConversation` variables for launching from a group row */ export type ConversationGroupLaunch = { workingDir?: string; diff --git a/src/components/features/conversation-panel/conversation-panel.tsx b/src/components/features/conversation-panel/conversation-panel.tsx index 4c9c29fa8..38d5c3dbd 100644 --- a/src/components/features/conversation-panel/conversation-panel.tsx +++ b/src/components/features/conversation-panel/conversation-panel.tsx @@ -33,6 +33,7 @@ import { ConversationPanelFilterMenu } from "./conversation-panel-filter-menu"; import { ConversationPanelNewThreadPicker } from "./conversation-panel-new-thread-picker"; import { groupConversations, + getGroupConversationPreview, sortConversationsByField, type ConversationGroupLaunch, } from "./conversation-panel-list-helpers"; @@ -140,6 +141,9 @@ export function ConversationPanel({ const [collapsedGroupIds, setCollapsedGroupIds] = React.useState< ReadonlySet >(() => new Set()); + const [expandedGroupPreviewIds, setExpandedGroupPreviewIds] = React.useState< + ReadonlySet + >(() => new Set()); const toggleGroupCollapsed = React.useCallback((groupId: string) => { setCollapsedGroupIds((prev) => { @@ -153,9 +157,22 @@ export function ConversationPanel({ }); }, []); + const toggleGroupPreviewExpanded = React.useCallback((groupId: string) => { + setExpandedGroupPreviewIds((prev) => { + const next = new Set(prev); + if (next.has(groupId)) { + next.delete(groupId); + } else { + next.add(groupId); + } + return next; + }); + }, []); + React.useEffect(() => { if (organizeMode !== "grouped") { setCollapsedGroupIds(new Set()); + setExpandedGroupPreviewIds(new Set()); } }, [organizeMode]); @@ -170,8 +187,14 @@ export function ConversationPanel({ string | null >(null); - const { data, isLoading, hasNextPage, isFetchingNextPage, fetchNextPage } = - usePaginatedConversations(); + const { + data, + isLoading, + isFetched, + hasNextPage, + isFetchingNextPage, + fetchNextPage, + } = usePaginatedConversations(); // Fetch in-progress start tasks const { data: startTasks } = useStartTasks(); @@ -419,7 +442,7 @@ export function ConversationPanel({ key={conversation.id} to={`/conversations/${conversation.id}`} onClick={onClose} - className="block py-0.5" + className="block" > @@ -475,13 +498,16 @@ export function ConversationPanel({ // child fills the panel and scrolls when its content overflows. Modals are // siblings of the scroll element and are `position: fixed`, so they don't // participate in the panel's scroll geometry. - // Gate on `isLoading` (true only during the first fetch with no cached - // data), not `isFetching` — the latter flips back to true on every 10s - // background refetch, causing the skeleton/empty-state to flicker when - // the list is empty. - const showInitialSkeleton = isLoading; + // Gate on `isLoading` / `!isFetched` (true only until the first fetch settles), + // not `isFetching` — the latter flips back to true on every 10s background + // refetch, causing the skeleton/empty-state to flicker when the list is empty. + const showInitialSkeleton = isLoading || !isFetched; const showEmptyState = - !isLoading && !compact && listIsEffectivelyEmpty && !startTasks?.length; + isFetched && + !isLoading && + !compact && + listIsEffectivelyEmpty && + !startTasks?.length; const showConversationHeader = !compact; @@ -550,8 +576,11 @@ export function ConversationPanel({ {showInitialSkeleton && } {!compact && showEmptyState && ( -
-

+

+

{t(I18nKey.CONVERSATION$NO_CONVERSATIONS)}

@@ -586,12 +615,22 @@ export function ConversationPanel({ > {conversationGroups.map((group) => { const headingId = `thread-folder-${group.id.replace(/[^a-zA-Z0-9_-]/g, "-")}`; + const groupTestIdSuffix = group.id.replace( + /[^a-zA-Z0-9_-]/g, + "-", + ); const expanded = !collapsedGroupIds.has(group.id); + const previewExpanded = expandedGroupPreviewIds.has(group.id); + const { visibleConversations, isPreviewTruncated, isShowingAll } = + getGroupConversationPreview(group.conversations, { + expanded: previewExpanded, + activeConversationId: currentConversationId, + }); return (
{expanded ? (
- {group.conversations.map(renderConversationCard)} + {visibleConversations.map(renderConversationCard)} + {isPreviewTruncated ? ( +
+ +
+ ) : null}
) : null}
@@ -648,7 +701,9 @@ export function ConversationPanel({ {!showInitialSkeleton && !compact && organizeMode === "chronological" ? ( - <>{sortedVisibleConversations.map(renderConversationCard)} +
+ {sortedVisibleConversations.map(renderConversationCard)} +
) : null} {/* Explicit "Load more" trigger. Only shown when more pages exist diff --git a/src/components/features/conversation-panel/ellipsis-button.tsx b/src/components/features/conversation-panel/ellipsis-button.tsx index e87c1a3d4..1aeea45bb 100644 --- a/src/components/features/conversation-panel/ellipsis-button.tsx +++ b/src/components/features/conversation-panel/ellipsis-button.tsx @@ -1,6 +1,7 @@ import React from "react"; import ThreeDotsVerticalIcon from "#/icons/three-dots-vertical.svg?react"; import { cn } from "#/utils/utils"; +import { formControlTransitionClassName } from "#/utils/form-control-classes"; interface EllipsisButtonProps { onClick: (event: React.MouseEvent) => void; @@ -49,7 +50,8 @@ export const EllipsisButton = React.forwardRef< onClick={onClick} aria-label={ariaLabel} className={cn( - "p-1 rounded-md cursor-pointer transition-colors", + "p-1 rounded-md cursor-pointer", + formControlTransitionClassName, "text-[var(--oh-muted)] hover:text-white hover:bg-white/10", "flex items-center justify-center", className, diff --git a/src/components/features/conversation-panel/new-conversation-button.tsx b/src/components/features/conversation-panel/new-conversation-button.tsx index d2613e256..dcea5eceb 100644 --- a/src/components/features/conversation-panel/new-conversation-button.tsx +++ b/src/components/features/conversation-panel/new-conversation-button.tsx @@ -58,7 +58,7 @@ export function NewConversationButton({ aria-label={compact ? newConversationLabel : undefined} className={cn( "flex items-center rounded-md cursor-pointer transition-colors", - "text-sm font-medium text-white bg-[var(--oh-surface)]/60 hover:bg-[var(--oh-surface-raised)]", + "text-sm text-white bg-[var(--oh-surface)]/60 hover:bg-[var(--oh-surface-raised)]", "border border-[var(--oh-border)]", compact ? "justify-center w-10 h-10 p-0 mx-auto" diff --git a/src/components/features/conversation-panel/skills-modal-section.tsx b/src/components/features/conversation-panel/skills-modal-section.tsx index a1bed46ee..f88d10e7a 100644 --- a/src/components/features/conversation-panel/skills-modal-section.tsx +++ b/src/components/features/conversation-panel/skills-modal-section.tsx @@ -16,7 +16,7 @@ export function SkillsModalSection({
- + {title} diff --git a/src/components/features/conversation-panel/system-message-modal/tab-button.tsx b/src/components/features/conversation-panel/system-message-modal/tab-button.tsx index 43c174064..649cd77b0 100644 --- a/src/components/features/conversation-panel/system-message-modal/tab-button.tsx +++ b/src/components/features/conversation-panel/system-message-modal/tab-button.tsx @@ -20,7 +20,7 @@ export function TabButton({ type="button" disabled={disabled} className={cn( - "px-4 py-2 font-medium border-b-2 transition-colors", + "px-4 py-2 font-normal border-b-2 transition-colors", isActive ? "border-foreground text-foreground" : "border-transparent text-[var(--oh-muted)] hover:text-[var(--oh-foreground)]", diff --git a/src/components/features/conversation/conversation-loading.tsx b/src/components/features/conversation/conversation-loading.tsx index d9904247d..86ec4ba10 100644 --- a/src/components/features/conversation/conversation-loading.tsx +++ b/src/components/features/conversation/conversation-loading.tsx @@ -21,7 +21,7 @@ export function ConversationLoading({ className }: ConversationLoadingProps) { className="h-8 w-8 shrink-0 animate-spin text-[var(--oh-text-secondary)]" aria-hidden /> - + {t(I18nKey.HOME$LOADING)}
diff --git a/src/components/features/conversation/conversation-name-context-menu-icon-text.tsx b/src/components/features/conversation/conversation-name-context-menu-icon-text.tsx index 7955c0132..982248257 100644 --- a/src/components/features/conversation/conversation-name-context-menu-icon-text.tsx +++ b/src/components/features/conversation/conversation-name-context-menu-icon-text.tsx @@ -1,4 +1,5 @@ import { cn } from "#/utils/utils"; +import { formControlTransitionClassName } from "#/utils/form-control-classes"; interface ConversationNameContextMenuIconTextProps { icon: React.ReactNode; @@ -14,7 +15,10 @@ export function ConversationNameContextMenuIconText({ return (
{icon} diff --git a/src/components/features/conversation/conversation-tabs/conversation-tab-nav.tsx b/src/components/features/conversation/conversation-tabs/conversation-tab-nav.tsx index ba4ba16b3..7e1b350ba 100644 --- a/src/components/features/conversation/conversation-tabs/conversation-tab-nav.tsx +++ b/src/components/features/conversation/conversation-tabs/conversation-tab-nav.tsx @@ -47,7 +47,7 @@ export function ConversationTabNav({ > {isActive && label && ( - {label} + {label} )} ); diff --git a/src/components/features/conversation/conversation-tabs/conversation-tabs.tsx b/src/components/features/conversation/conversation-tabs/conversation-tabs.tsx index 5a0f72e7d..76c37de8a 100644 --- a/src/components/features/conversation/conversation-tabs/conversation-tabs.tsx +++ b/src/components/features/conversation/conversation-tabs/conversation-tabs.tsx @@ -352,7 +352,7 @@ export function ConversationTabs({ )} data-testid="planner-tab-build-button" > - + {/* eslint-disable-next-line i18next/no-literal-string */} {t(I18nKey.COMMON$BUILD)} ⌘↩ diff --git a/src/components/features/home/git-branch-dropdown/git-branch-dropdown.tsx b/src/components/features/home/git-branch-dropdown/git-branch-dropdown.tsx index 45f72916e..8d5020456 100644 --- a/src/components/features/home/git-branch-dropdown/git-branch-dropdown.tsx +++ b/src/components/features/home/git-branch-dropdown/git-branch-dropdown.tsx @@ -10,6 +10,7 @@ import { Branch } from "#/types/git"; import { Provider } from "#/types/settings"; import { useDebounce } from "#/hooks/use-debounce"; import { cn } from "#/utils/utils"; +import { formControlFieldClassName } from "#/utils/form-control-classes"; import { useBranchData } from "#/hooks/query/use-branch-data"; import { ClearButton } from "../shared/clear-button"; @@ -183,7 +184,7 @@ export function GitBranchDropdown({ return (
-
+
{isLoadingState ? (
@@ -196,11 +197,10 @@ export function GitBranchDropdown({ disabled: disabled || !repository, placeholder, className: cn( - "w-full px-3 py-2 border border-[var(--oh-border-input)] rounded-sm shadow-none h-[42px] min-h-[42px] max-h-[42px]", - "bg-tertiary text-[var(--oh-muted)] placeholder:text-[var(--oh-muted)]", - "focus:outline-none focus:ring-0 focus:border-[var(--oh-border-input)]", - "disabled:bg-tertiary disabled:cursor-not-allowed disabled:opacity-60", - "pl-7 pr-16 text-sm font-normal leading-5", // Space for clear and toggle buttons + formControlFieldClassName, + "text-inherit shadow-none pl-7 pr-16 text-sm font-normal leading-5", + "placeholder:text-[var(--oh-muted)]", + "disabled:cursor-not-allowed disabled:opacity-60", ), // Direct onChange for cursor position preservation onChange: (e: React.ChangeEvent) => { @@ -219,7 +219,6 @@ export function GitBranchDropdown({ isOpen={isOpen} disabled={disabled || !repository} getToggleButtonProps={getToggleButtonProps} - iconClassName="w-10 h-10" />
diff --git a/src/components/features/home/git-provider-dropdown/git-provider-dropdown.tsx b/src/components/features/home/git-provider-dropdown/git-provider-dropdown.tsx index 023cfa062..a4b61ea60 100644 --- a/src/components/features/home/git-provider-dropdown/git-provider-dropdown.tsx +++ b/src/components/features/home/git-provider-dropdown/git-provider-dropdown.tsx @@ -156,7 +156,7 @@ export function GitProviderDropdown({ return (
-
+
{/* Provider icon */} {selectedItem && (
@@ -174,7 +174,7 @@ export function GitProviderDropdown({ readOnly: true, // Make it non-searchable like the original className: cn( "w-29.5 h-6 py-0 border border-[var(--oh-border-input)] rounded shadow-none h-6 min-h-6 max-h-6 ", - "bg-tertiary text-[var(--oh-muted)] placeholder:text-[var(--oh-muted)]", + "text-inherit bg-tertiary placeholder:text-[var(--oh-muted)]", "focus:outline-none focus:ring-0 focus:border-[var(--oh-border-input)]", "disabled:bg-tertiary disabled:cursor-not-allowed disabled:opacity-60", "pl-1.5 pr-[1px] cursor-pointer text-xs font-normal leading-5", // Space for toggle button and pointer cursor @@ -190,10 +190,7 @@ export function GitProviderDropdown({ isOpen={isOpen} disabled={disabled} getToggleButtonProps={getToggleButtonProps} - iconClassName={cn( - "w-[23px] h-[23px] translate-y-[1px]", - toggleButtonClassName, - )} + iconClassName={toggleButtonClassName} />
diff --git a/src/components/features/home/git-repo-dropdown/git-repo-dropdown.tsx b/src/components/features/home/git-repo-dropdown/git-repo-dropdown.tsx index 27e5657c0..13dce4918 100644 --- a/src/components/features/home/git-repo-dropdown/git-repo-dropdown.tsx +++ b/src/components/features/home/git-repo-dropdown/git-repo-dropdown.tsx @@ -11,6 +11,7 @@ import { Provider } from "#/types/settings"; import { GitRepository } from "#/types/git"; import { useDebounce } from "#/hooks/use-debounce"; import { cn } from "#/utils/utils"; +import { formControlFieldClassName } from "#/utils/form-control-classes"; import { ClearButton } from "../shared/clear-button"; import { ToggleButton } from "../shared/toggle-button"; @@ -286,7 +287,7 @@ export function GitRepoDropdown({ return (
-
+
{isLoadingState ? (
@@ -299,11 +300,10 @@ export function GitRepoDropdown({ disabled, placeholder, className: cn( - "w-full px-3 py-2 border border-[var(--oh-border-input)] rounded-sm shadow-none h-[42px] min-h-[42px] max-h-[42px]", - "bg-tertiary text-[var(--oh-muted)] placeholder:text-[var(--oh-muted)]", - "focus:outline-none focus:ring-0 focus:border-[var(--oh-border-input)]", - "disabled:bg-tertiary disabled:cursor-not-allowed disabled:opacity-60", - "pl-7 pr-16 text-sm font-normal leading-5", // Space for clear and toggle buttons + formControlFieldClassName, + "text-inherit shadow-none pl-7 pr-16 text-sm font-normal leading-5", + "placeholder:text-[var(--oh-muted)]", + "disabled:cursor-not-allowed disabled:opacity-60", ), // Direct onChange for cursor position preservation onChange: (e: React.ChangeEvent) => { @@ -322,7 +322,6 @@ export function GitRepoDropdown({ isOpen={isOpen} disabled={disabled} getToggleButtonProps={getToggleButtonProps} - iconClassName="w-10 h-10" />
diff --git a/src/components/features/home/home-header/home-header-title.tsx b/src/components/features/home/home-header/home-header-title.tsx index 8cc478da4..db840d556 100644 --- a/src/components/features/home/home-header/home-header-title.tsx +++ b/src/components/features/home/home-header/home-header-title.tsx @@ -6,7 +6,7 @@ export function HomeHeaderTitle() { return (
- + {t("HOME$LETS_START_BUILDING")}
diff --git a/src/components/features/home/new-conversation.tsx b/src/components/features/home/new-conversation.tsx index 0c508b26d..767483e8a 100644 --- a/src/components/features/home/new-conversation.tsx +++ b/src/components/features/home/new-conversation.tsx @@ -51,7 +51,7 @@ export function NewConversation() { ) } isDisabled={isCreatingConversation} - className="w-auto absolute bottom-5 left-5 right-5 font-semibold" + className="w-auto absolute bottom-5 left-5 right-5" > {!isCreatingConversation && t("COMMON$NEW_CONVERSATION")} {isCreatingConversation && t("HOME$LOADING")} diff --git a/src/components/features/home/new-conversation/create-conversation-button.tsx b/src/components/features/home/new-conversation/create-conversation-button.tsx index e417a90e2..1a26f3e35 100644 --- a/src/components/features/home/new-conversation/create-conversation-button.tsx +++ b/src/components/features/home/new-conversation/create-conversation-button.tsx @@ -36,7 +36,7 @@ export function CreateConversationButton() { type="button" onClick={handleCreateConversation} isDisabled={isCreatingConversation} - className="w-auto absolute bottom-5 left-5 right-5 font-semibold" + className="w-auto absolute bottom-5 left-5 right-5" > {!isCreatingConversation && t("COMMON$NEW_CONVERSATION")} {isCreatingConversation && t("HOME$LOADING")} diff --git a/src/components/features/home/open-launcher-button.tsx b/src/components/features/home/open-launcher-button.tsx index 3d4756036..6fb4a0e7c 100644 --- a/src/components/features/home/open-launcher-button.tsx +++ b/src/components/features/home/open-launcher-button.tsx @@ -1,8 +1,13 @@ +import { Folder } from "lucide-react"; import { useTranslation } from "react-i18next"; import { I18nKey } from "#/i18n/declaration"; -import FolderIcon from "#/icons/folder.svg?react"; import RepoForkedIcon from "#/icons/repo-forked.svg?react"; import { cn } from "#/utils/utils"; +import { + formControlBorderClassName, + formControlSurfaceClassName, + formControlTransitionClassName, +} from "#/utils/form-control-classes"; interface OpenLauncherButtonProps { kind: "local" | "cloud"; @@ -30,20 +35,23 @@ export function OpenLauncherButton({ onClick={onClick} disabled={disabled} className={cn( - "flex flex-row items-center gap-2 pl-2.5 pr-2.5 py-1 rounded-[100px] border border-[rgba(71,74,84,0.50)] bg-transparent", + "flex flex-row items-center gap-2 rounded-full px-2.5 py-1 text-white", + formControlBorderClassName, + formControlSurfaceClassName, + formControlTransitionClassName, disabled - ? "opacity-50 cursor-not-allowed" - : "hover:border-[var(--oh-border-subtle)] cursor-pointer", + ? "cursor-not-allowed opacity-50" + : "cursor-pointer hover:bg-surface-raised", )} > - + {isLocal ? ( - + ) : ( - + )} - {label} + {label} ); } diff --git a/src/components/features/home/repo-selection-form.tsx b/src/components/features/home/repo-selection-form.tsx index a4887efb8..1074827c5 100644 --- a/src/components/features/home/repo-selection-form.tsx +++ b/src/components/features/home/repo-selection-form.tsx @@ -240,7 +240,7 @@ export function RepositorySelectionForm({ }, ); }} - className="w-full font-semibold" + className="w-full" > {onConfirm ? t(I18nKey.BUTTON$CONFIRM) diff --git a/src/components/features/home/repository-selection/branch-error-state.tsx b/src/components/features/home/repository-selection/branch-error-state.tsx index defa4873b..23d10128a 100644 --- a/src/components/features/home/repository-selection/branch-error-state.tsx +++ b/src/components/features/home/repository-selection/branch-error-state.tsx @@ -1,5 +1,6 @@ import { useTranslation } from "react-i18next"; import { cn } from "#/utils/utils"; +import { formControlFieldClassName } from "#/utils/form-control-classes"; interface BranchErrorStateProps { wrapperClassName?: string; @@ -11,7 +12,8 @@ export function BranchErrorState({ wrapperClassName }: BranchErrorStateProps) {
diff --git a/src/components/features/home/repository-selection/branch-loading-state.tsx b/src/components/features/home/repository-selection/branch-loading-state.tsx index 6359aacc2..ed998d70d 100644 --- a/src/components/features/home/repository-selection/branch-loading-state.tsx +++ b/src/components/features/home/repository-selection/branch-loading-state.tsx @@ -1,6 +1,7 @@ import { useTranslation } from "react-i18next"; import { Spinner } from "@heroui/react"; import { cn } from "#/utils/utils"; +import { formControlFieldClassName } from "#/utils/form-control-classes"; interface BranchLoadingStateProps { wrapperClassName?: string; @@ -14,7 +15,8 @@ export function BranchLoadingState({
diff --git a/src/components/features/home/repository-selection/repository-error-state.tsx b/src/components/features/home/repository-selection/repository-error-state.tsx index 1d4c8f556..90159d284 100644 --- a/src/components/features/home/repository-selection/repository-error-state.tsx +++ b/src/components/features/home/repository-selection/repository-error-state.tsx @@ -1,5 +1,6 @@ import { useTranslation } from "react-i18next"; import { cn } from "#/utils/utils"; +import { formControlFieldClassName } from "#/utils/form-control-classes"; export interface RepositoryErrorStateProps { wrapperClassName?: string; @@ -13,7 +14,8 @@ export function RepositoryErrorState({
diff --git a/src/components/features/home/repository-selection/repository-loading-state.tsx b/src/components/features/home/repository-selection/repository-loading-state.tsx index e26de2f19..510466932 100644 --- a/src/components/features/home/repository-selection/repository-loading-state.tsx +++ b/src/components/features/home/repository-selection/repository-loading-state.tsx @@ -1,6 +1,7 @@ import { useTranslation } from "react-i18next"; import { Spinner } from "@heroui/react"; import { cn } from "#/utils/utils"; +import { formControlFieldClassName } from "#/utils/form-control-classes"; export interface RepositoryLoadingStateProps { wrapperClassName?: string; @@ -14,7 +15,8 @@ export function RepositoryLoadingState({
diff --git a/src/components/features/home/shared/dropdown-item.tsx b/src/components/features/home/shared/dropdown-item.tsx index c39fc94a7..05cded7fb 100644 --- a/src/components/features/home/shared/dropdown-item.tsx +++ b/src/components/features/home/shared/dropdown-item.tsx @@ -44,7 +44,7 @@ export function DropdownItem({
  • {renderIcon?.(item)} - {getDisplayText(item)} + {getDisplayText(item)}
  • ); diff --git a/src/components/features/home/shared/toggle-button.tsx b/src/components/features/home/shared/toggle-button.tsx index b074abb84..e38af6e60 100644 --- a/src/components/features/home/shared/toggle-button.tsx +++ b/src/components/features/home/shared/toggle-button.tsx @@ -1,5 +1,8 @@ +import { + ComboboxCaretIcon, + comboboxCaretButtonClassName, +} from "#/ui/combobox-caret"; import { cn } from "#/utils/utils"; -import ChevronDownSmallIcon from "#/icons/chevron-down-small.svg?react"; interface ToggleButtonProps { isOpen: boolean; @@ -21,20 +24,16 @@ export function ToggleButton({ {...getToggleButtonProps({ disabled, className: cn( - "text-[#fff]", - "disabled:cursor-not-allowed disabled:opacity-60", + comboboxCaretButtonClassName, + "text-current", + isOpen && "rotate-180", + disabled && "cursor-not-allowed opacity-60", ), })} type="button" aria-label="Toggle menu" > - + ); } diff --git a/src/components/features/home/tasks/task-suggestions.tsx b/src/components/features/home/tasks/task-suggestions.tsx index f09c14b42..cdd6b254a 100644 --- a/src/components/features/home/tasks/task-suggestions.tsx +++ b/src/components/features/home/tasks/task-suggestions.tsx @@ -58,7 +58,7 @@ export function TaskSuggestions({ filterFor }: TaskSuggestionsProps) { !hasSuggestedTasks && "mb-[14px]", )} > -

    +

    {t(I18nKey.TASKS$SUGGESTED_TASKS)}

    diff --git a/src/components/features/home/workspace-dropdown/folder-browser-modal.tsx b/src/components/features/home/workspace-dropdown/folder-browser-modal.tsx index 4b0e96c3a..a5297b424 100644 --- a/src/components/features/home/workspace-dropdown/folder-browser-modal.tsx +++ b/src/components/features/home/workspace-dropdown/folder-browser-modal.tsx @@ -218,7 +218,7 @@ export function FolderBrowserModal({ {/* Title bar */}
    diff --git a/src/components/features/home/workspace-dropdown/manage-workspaces-modal.tsx b/src/components/features/home/workspace-dropdown/manage-workspaces-modal.tsx index 14bfe4a81..fbbb1c47d 100644 --- a/src/components/features/home/workspace-dropdown/manage-workspaces-modal.tsx +++ b/src/components/features/home/workspace-dropdown/manage-workspaces-modal.tsx @@ -83,7 +83,7 @@ export function ManageWorkspacesModal({ >
    diff --git a/src/components/features/home/workspace-dropdown/workspace-dropdown.tsx b/src/components/features/home/workspace-dropdown/workspace-dropdown.tsx index 3868803b7..06b722a90 100644 --- a/src/components/features/home/workspace-dropdown/workspace-dropdown.tsx +++ b/src/components/features/home/workspace-dropdown/workspace-dropdown.tsx @@ -3,6 +3,7 @@ import { useCombobox } from "downshift"; import { useTranslation } from "react-i18next"; import { cn } from "#/utils/utils"; +import { formControlFieldClassName } from "#/utils/form-control-classes"; import { LocalWorkspace } from "#/types/workspace"; import { I18nKey } from "#/i18n/declaration"; import RepoIcon from "#/icons/repo.svg?react"; @@ -168,7 +169,7 @@ export function WorkspaceDropdown({ return (
    -
    +
    @@ -177,11 +178,10 @@ export function WorkspaceDropdown({ disabled, placeholder: placeholder ?? t(I18nKey.HOME$WORKSPACE_PLACEHOLDER), className: cn( - "w-full px-3 py-2 border border-[var(--oh-border-input)] rounded-sm shadow-none h-[42px] min-h-[42px] max-h-[42px]", - "bg-tertiary text-[var(--oh-muted)] placeholder:text-[var(--oh-muted)]", - "focus:outline-none focus:ring-0 focus:border-[var(--oh-border-input)]", - "disabled:bg-tertiary disabled:cursor-not-allowed disabled:opacity-60", - "pl-7 pr-16 text-sm font-normal leading-5", + formControlFieldClassName, + "text-inherit shadow-none pl-7 pr-16 text-sm font-normal leading-5", + "placeholder:text-[var(--oh-muted)]", + "disabled:cursor-not-allowed disabled:opacity-60", ), onChange: (e: React.ChangeEvent) => { setInputValue(e.target.value); @@ -196,7 +196,6 @@ export function WorkspaceDropdown({ isOpen={isOpen} disabled={disabled} getToggleButtonProps={getToggleButtonProps} - iconClassName="w-10 h-10" />
    diff --git a/src/components/features/home/workspace-selection-form.tsx b/src/components/features/home/workspace-selection-form.tsx index 2ae5d34bc..70e68c679 100644 --- a/src/components/features/home/workspace-selection-form.tsx +++ b/src/components/features/home/workspace-selection-form.tsx @@ -136,9 +136,7 @@ export function WorkspaceSelectionForm({ } onClick={handleLaunch} className={ - onConfirm - ? "w-full font-semibold" - : "w-auto absolute bottom-5 left-5 right-5 font-semibold" + onConfirm ? "w-full" : "w-auto absolute bottom-5 left-5 right-5" } > {onConfirm diff --git a/src/components/features/launch/plugin-launch-plugin-section.tsx b/src/components/features/launch/plugin-launch-plugin-section.tsx index c0e8dd459..7dd228ae9 100644 --- a/src/components/features/launch/plugin-launch-plugin-section.tsx +++ b/src/components/features/launch/plugin-launch-plugin-section.tsx @@ -42,7 +42,7 @@ export function PluginLaunchPluginSection({ className="flex w-full items-center justify-between px-4 py-3 text-left hover:bg-base-tertiary rounded-t-lg cursor-pointer" data-testid={`plugin-section-${originalIndex}`} > - + {getPluginDisplayName(plugin)} {isExpanded ? ( diff --git a/src/components/features/mcp-page/custom-server-editor.tsx b/src/components/features/mcp-page/custom-server-editor.tsx index 69c67ef81..ae93e0df6 100644 --- a/src/components/features/mcp-page/custom-server-editor.tsx +++ b/src/components/features/mcp-page/custom-server-editor.tsx @@ -87,7 +87,7 @@ export function CustomServerEditor({ >
    -

    - {entry.name} -

    +

    {entry.name}

    {entry.description}

    diff --git a/src/components/features/mcp-page/marketplace-section.tsx b/src/components/features/mcp-page/marketplace-section.tsx index 1727402ae..e45650773 100644 --- a/src/components/features/mcp-page/marketplace-section.tsx +++ b/src/components/features/mcp-page/marketplace-section.tsx @@ -40,7 +40,7 @@ export function MarketplaceSection({ data-testid="mcp-marketplace-section" className="flex flex-col gap-3" > -

    +

    {t(I18nKey.MCP$LIBRARY_TITLE)}

    diff --git a/src/components/features/onboarding/steps/check-backend-step.tsx b/src/components/features/onboarding/steps/check-backend-step.tsx index c3f00f686..f717d3884 100644 --- a/src/components/features/onboarding/steps/check-backend-step.tsx +++ b/src/components/features/onboarding/steps/check-backend-step.tsx @@ -90,7 +90,7 @@ export function CheckBackendStep({ onBack, onNext }: CheckBackendStepProps) { className="flex flex-col gap-6" >
    -

    +

    {t(I18nKey.ONBOARDING$BACKEND_TITLE)}

    diff --git a/src/components/features/onboarding/steps/choose-agent-step.tsx b/src/components/features/onboarding/steps/choose-agent-step.tsx index 313dcb585..d9452df81 100644 --- a/src/components/features/onboarding/steps/choose-agent-step.tsx +++ b/src/components/features/onboarding/steps/choose-agent-step.tsx @@ -187,7 +187,7 @@ export function ChooseAgentStep({ className="flex flex-col gap-6" >

    -

    +

    {t(I18nKey.ONBOARDING$AGENT_TITLE)}

    @@ -221,7 +221,7 @@ export function ChooseAgentStep({

    - + {option.label}
    diff --git a/src/components/features/onboarding/steps/say-hello-step.tsx b/src/components/features/onboarding/steps/say-hello-step.tsx index fc68f65d5..7688cb54b 100644 --- a/src/components/features/onboarding/steps/say-hello-step.tsx +++ b/src/components/features/onboarding/steps/say-hello-step.tsx @@ -69,7 +69,7 @@ export function SayHelloStep({ onBack, onLaunched }: SayHelloStepProps) { className="flex flex-col gap-6" >
    -

    +

    {t(I18nKey.ONBOARDING$HELLO_TITLE)}

    diff --git a/src/components/features/onboarding/steps/setup-llm-step.tsx b/src/components/features/onboarding/steps/setup-llm-step.tsx index fb403c8b1..3c47677cd 100644 --- a/src/components/features/onboarding/steps/setup-llm-step.tsx +++ b/src/components/features/onboarding/steps/setup-llm-step.tsx @@ -107,7 +107,7 @@ export function SetupLlmStep({ onBack, onNext }: SetupLlmStepProps) { className="flex flex-col gap-6 max-h-[calc(90vh-7rem)]" >

    -

    +

    {t(I18nKey.ONBOARDING$LLM_TITLE)}

    diff --git a/src/components/features/settings/api-key-modal-base.tsx b/src/components/features/settings/api-key-modal-base.tsx index 584d9e2e6..0617b149a 100644 --- a/src/components/features/settings/api-key-modal-base.tsx +++ b/src/components/features/settings/api-key-modal-base.tsx @@ -92,7 +92,11 @@ export function ApiKeyModalBase({ MODAL_MAX_WIDTH_VIEWPORT, )} > - + {children}

    {footer}
    diff --git a/src/components/features/settings/brand-button.tsx b/src/components/features/settings/brand-button.tsx index 48ddaff67..2fced8705 100644 --- a/src/components/features/settings/brand-button.tsx +++ b/src/components/features/settings/brand-button.tsx @@ -1,5 +1,6 @@ import { forwardRef } from "react"; import { cn } from "#/utils/utils"; +import { formControlButtonClassName } from "#/utils/form-control-classes"; interface BrandButtonProps { testId?: string; @@ -48,16 +49,16 @@ export const BrandButton = forwardRef< aria-label={ariaLabel} aria-busy={ariaBusy} className={cn( - "w-fit p-2 text-sm rounded-sm disabled:opacity-30 disabled:cursor-not-allowed cursor-pointer", + formControlButtonClassName, variant === "primary" && "bg-primary text-[var(--oh-color-base)] hover:opacity-80", variant === "secondary" && - "border border-[var(--oh-border)] text-white hover:bg-surface-raised", + "border border-[var(--oh-border)] bg-base-secondary text-white hover:bg-surface-raised", variant === "tertiary" && "bg-[var(--oh-interactive-hover)] text-white hover:opacity-80", variant === "danger" && "bg-red-600 text-white hover:bg-red-700", variant === "ghost-danger" && - "bg-transparent text-red-600 underline hover:text-red-700 hover:no-underline font-medium", + "h-auto min-h-0 bg-transparent px-0 text-red-600 underline hover:text-red-700 hover:no-underline font-normal", startContent && "flex items-center justify-center gap-2", className, )} diff --git a/src/components/features/settings/llm-profiles/llm-profiles-manager.tsx b/src/components/features/settings/llm-profiles/llm-profiles-manager.tsx index af646690d..85e87e9d1 100644 --- a/src/components/features/settings/llm-profiles/llm-profiles-manager.tsx +++ b/src/components/features/settings/llm-profiles/llm-profiles-manager.tsx @@ -53,7 +53,7 @@ export function LlmProfilesManager({ <>
    -

    +

    {t(I18nKey.SETTINGS$AVAILABLE_PROFILES)}

    {onAddProfile ? ( diff --git a/src/components/features/settings/llm-profiles/llm-settings-local-view.tsx b/src/components/features/settings/llm-profiles/llm-settings-local-view.tsx index b60ac0de9..d83a23757 100644 --- a/src/components/features/settings/llm-profiles/llm-settings-local-view.tsx +++ b/src/components/features/settings/llm-profiles/llm-settings-local-view.tsx @@ -1,4 +1,4 @@ -import React, { useState, useCallback, useMemo } from "react"; +import React, { useState, useCallback, useMemo, useEffect } from "react"; import { useTranslation } from "react-i18next"; import { LlmProfilesManager } from "./llm-profiles-manager"; import { ProfileNameInput } from "./profile-name-input"; @@ -21,6 +21,8 @@ import { import { SdkSectionSaveControl } from "../sdk-settings/sdk-section-page"; import { SettingsFormValues } from "#/utils/sdk-settings-schema"; import { ArrowLeft } from "lucide-react"; +import { Typography } from "#/ui/typography"; +import { useSettingsSectionHeader } from "#/contexts/settings-section-header-context"; type ViewMode = "list" | "create" | "edit"; @@ -41,6 +43,7 @@ interface EditingProfile { export function LlmSettingsLocalView() { const { t } = useTranslation("openhands"); + const { setHideSectionHeader } = useSettingsSectionHeader(); const saveProfile = useSaveLlmProfile(); const { data: profilesData } = useLlmProfiles(); @@ -54,6 +57,11 @@ export function LlmSettingsLocalView() { ); const [isSaving, setIsSaving] = useState(false); + useEffect(() => { + setHideSectionHeader(viewMode !== "list"); + return () => setHideSectionHeader(false); + }, [viewMode, setHideSectionHeader]); + // Get existing profile names for validation const existingNames = useMemo( () => new Set(profilesData?.profiles.map((p) => p.name) ?? []), @@ -268,20 +276,18 @@ export function LlmSettingsLocalView() {
    {/* Header with back button */}
    -
    - -

    - {profileEditorTitle} -

    -
    + + + {profileEditorTitle} +

    void; onKeyDown: (e: React.KeyboardEvent, index: number) => void; menuItemsRef: React.MutableRefObject<(HTMLButtonElement | null)[]>; disabled?: boolean; - className?: string; testId: string; - destructive?: boolean; } function MenuItem({ index, + icon, label, onClick, onKeyDown, menuItemsRef, disabled, - className, testId, - destructive, }: MenuItemProps) { return ( ); } @@ -97,13 +99,13 @@ export function ProfileActionsMenu({ const updatePosition = () => { const rect = anchorElement.getBoundingClientRect(); if (!rect) return; - // 4px gap matches the previous `mt-1` spacing. - const gap = 4; + const gap = 8; setPortalStyle({ position: "fixed", zIndex: 9999, top: rect.bottom + gap, right: window.innerWidth - rect.right, + width: "max-content", }); }; @@ -176,11 +178,7 @@ export function ProfileActionsMenu({

    } label={t(I18nKey.SETTINGS$PROFILE_EDIT)} onClick={() => handleAction(onEdit)} onKeyDown={handleKeyDown} @@ -198,6 +197,7 @@ export function ProfileActionsMenu({ /> } label={t(I18nKey.BUTTON$RENAME)} onClick={() => handleAction(onRename)} onKeyDown={handleKeyDown} @@ -206,6 +206,7 @@ export function ProfileActionsMenu({ /> } label={t(I18nKey.SETTINGS$PROFILE_SET_ACTIVE)} onClick={() => handleAction(onSetActive)} onKeyDown={handleKeyDown} @@ -215,13 +216,12 @@ export function ProfileActionsMenu({ /> } label={t(I18nKey.BUTTON$DELETE)} onClick={() => handleAction(onDelete)} onKeyDown={handleKeyDown} menuItemsRef={menuItemsRef} - className="text-red-400" testId="profile-delete" - destructive />
    ); diff --git a/src/components/features/settings/llm-profiles/profile-row.tsx b/src/components/features/settings/llm-profiles/profile-row.tsx index 2d049c8c3..e5490eecc 100644 --- a/src/components/features/settings/llm-profiles/profile-row.tsx +++ b/src/components/features/settings/llm-profiles/profile-row.tsx @@ -5,6 +5,11 @@ import { ProfileInfo } from "#/api/profiles-service/profiles-service.api"; import { I18nKey } from "#/i18n/declaration"; import { EllipsisButton } from "#/components/features/conversation-panel/ellipsis-button"; import { BrandBadge } from "#/components/shared/badge"; +import { cn } from "#/utils/utils"; +import { + settingsListIconActionButtonClassName, + settingsListRowClassName, +} from "#/utils/settings-list-classes"; interface ProfileRowProps { profile: ProfileInfo; @@ -32,18 +37,18 @@ export function ProfileRow({ return (
    -
    +
    {profile.name} {profile.model ? ( {profile.model} @@ -51,7 +56,7 @@ export function ProfileRow({ ) : null} {isActive && ( {t(I18nKey.SETTINGS$PROFILE_ACTIVE)} @@ -64,6 +69,7 @@ export function ProfileRow({ onClick={() => setMenuOpen((open) => !open)} ariaLabel={t(I18nKey.SETTINGS$PROFILE_MENU)} testId="profile-menu-trigger" + className={settingsListIconActionButtonClassName} /> {menuOpen && ( +
    {profiles.map((profile) => (

    @@ -399,9 +401,9 @@ export function MCPServerForm({ defaultValue={formatEnvironmentVariables(server?.env)} placeholder="KEY1=value1 KEY2=value2" className={cn( - "resize-none", - "bg-tertiary border border-[var(--oh-border-input)] rounded-sm p-2 placeholder:text-tertiary-alt", - "disabled:bg-[var(--oh-surface-raised)] disabled:border-[var(--oh-border-subtle)] disabled:cursor-not-allowed", + formControlMultilineFieldClassName, + "resize-none placeholder:italic", + "disabled:bg-[var(--oh-surface-raised)] disabled:border-[var(--oh-border-subtle)]", )} /> diff --git a/src/components/features/settings/sdk-settings/schema-field.tsx b/src/components/features/settings/sdk-settings/schema-field.tsx index 294e80a59..f69e0b8df 100644 --- a/src/components/features/settings/sdk-settings/schema-field.tsx +++ b/src/components/features/settings/sdk-settings/schema-field.tsx @@ -14,6 +14,10 @@ import { resolveSchemaFieldLabel, } from "#/utils/sdk-settings-field-metadata"; import { cn } from "#/utils/utils"; +import { + formControlMultilineFieldClassName, + formControlSwitchDescriptionClassName, +} from "#/utils/form-control-classes"; // --------------------------------------------------------------------------- // Help links – UI-only mapping from field keys to user-facing guidance. @@ -117,7 +121,9 @@ export function SchemaField({ > {label} - +

    + +
    ); } @@ -167,9 +173,9 @@ export function SchemaField({ disabled={isDisabled} onChange={(event) => onChange(event.target.value)} className={cn( - "bg-tertiary border border-[var(--oh-border-input)] min-h-32 w-full min-w-0 rounded-sm p-2 font-mono text-sm", - "placeholder:text-tertiary-alt", - "disabled:bg-[var(--oh-surface-raised)] disabled:border-[var(--oh-border-subtle)] disabled:cursor-not-allowed", + formControlMultilineFieldClassName, + "min-h-32 font-mono placeholder:italic", + "disabled:bg-[var(--oh-surface-raised)] disabled:border-[var(--oh-border-subtle)]", )} /> diff --git a/src/components/features/settings/sdk-settings/view-toggle.tsx b/src/components/features/settings/sdk-settings/view-toggle.tsx index 70f9591b8..83a9dbe45 100644 --- a/src/components/features/settings/sdk-settings/view-toggle.tsx +++ b/src/components/features/settings/sdk-settings/view-toggle.tsx @@ -2,6 +2,7 @@ import { useTranslation } from "react-i18next"; import { I18nKey } from "#/i18n/declaration"; import { SettingsView } from "#/utils/sdk-settings-schema"; import { cn } from "#/utils/utils"; +import { formControlTransitionClassName } from "#/utils/form-control-classes"; interface ViewToggleProps { view: SettingsView; @@ -13,7 +14,8 @@ interface ViewToggleProps { const tabButtonClass = (isActive: boolean, isDisabled: boolean) => cn( - "w-fit px-2 py-2 text-sm cursor-pointer rounded-none bg-transparent transition-[color,border-color]", + "w-fit px-2 py-2 text-sm cursor-pointer rounded-none bg-transparent", + formControlTransitionClassName, "border-b-2 pb-2", isActive ? "text-white border-white" diff --git a/src/components/features/settings/secrets-settings/secret-form.tsx b/src/components/features/settings/secrets-settings/secret-form.tsx index 1b63ea8db..6cffc4ff6 100644 --- a/src/components/features/settings/secrets-settings/secret-form.tsx +++ b/src/components/features/settings/secrets-settings/secret-form.tsx @@ -6,6 +6,10 @@ import { useCreateSecret } from "#/hooks/mutation/use-create-secret"; import { useUpdateSecret } from "#/hooks/mutation/use-update-secret"; import { SettingsInput } from "../settings-input"; import { cn } from "#/utils/utils"; +import { + formControlMultilineFieldClassName, + formControlSettingsFieldClassName, +} from "#/utils/form-control-classes"; import { BrandButton } from "../brand-button"; import { useSearchSecrets } from "#/hooks/query/use-get-secrets"; import { OptionalTag } from "../optional-tag"; @@ -138,7 +142,8 @@ export function SecretForm({ required className={cn( "resize-none", - "bg-tertiary border border-[var(--oh-border-input)] rounded-sm p-2 placeholder:text-tertiary-alt", + formControlMultilineFieldClassName, + "placeholder:italic", "disabled:bg-[var(--oh-surface-raised)] disabled:border-[var(--oh-border-subtle)] disabled:cursor-not-allowed", )} rows={8} @@ -156,9 +161,8 @@ export function SecretForm({ name="secret-description" defaultValue={secretDescription} className={cn( - "resize-none", - "bg-tertiary border border-[var(--oh-border-input)] rounded-sm p-2 placeholder:text-tertiary-alt", - "disabled:bg-[var(--oh-surface-raised)] disabled:border-[var(--oh-border-subtle)] disabled:cursor-not-allowed", + formControlSettingsFieldClassName, + "disabled:bg-[var(--oh-surface-raised)] disabled:border-[var(--oh-border-subtle)]", )} /> diff --git a/src/components/features/settings/secrets-settings/secret-list-item.tsx b/src/components/features/settings/secrets-settings/secret-list-item.tsx index 81d14af40..e89205b08 100644 --- a/src/components/features/settings/secrets-settings/secret-list-item.tsx +++ b/src/components/features/settings/secrets-settings/secret-list-item.tsx @@ -1,13 +1,24 @@ import { Pencil, Trash2 } from "lucide-react"; +import { cn } from "#/utils/utils"; +import { + settingsListIconActionButtonClassName, + settingsListRowClassName, + settingsListTableCellClassName, + settingsListTableRowClassName, +} from "#/utils/settings-list-classes"; export function SecretListItemSkeleton() { return ( -
    -
    - +
    +
    +
    -
    @@ -30,29 +41,35 @@ export function SecretListItem({ onDelete, }: SecretListItemProps) { return ( - - + + {title} {description || ""} - +
    @@ -61,7 +78,7 @@ export function SecretListItem({ type="button" onClick={onDelete} aria-label={`Delete ${title}`} - className="inline-flex cursor-pointer items-center justify-center rounded-md p-1 text-muted transition-colors hover:bg-interactive-hover hover:text-white" + className={settingsListIconActionButtonClassName} > diff --git a/src/components/features/settings/settings-dropdown-input.tsx b/src/components/features/settings/settings-dropdown-input.tsx index ac3a2bebe..603627805 100644 --- a/src/components/features/settings/settings-dropdown-input.tsx +++ b/src/components/features/settings/settings-dropdown-input.tsx @@ -3,6 +3,8 @@ import React, { ReactNode } from "react"; import { useTranslation } from "react-i18next"; import { OptionalTag } from "./optional-tag"; import { cn } from "#/utils/utils"; +import { formControlSettingsFieldClassName } from "#/utils/form-control-classes"; +import { heroUiAutocompleteSelectorButtonClassName } from "#/ui/combobox-caret"; interface SettingsDropdownInputProps { testId: string; @@ -79,14 +81,13 @@ export function SettingsDropdownInput({ className="w-full" classNames={{ popoverContent: "bg-content1 rounded-xl", - selectorButton: - "!rounded-none !bg-transparent data-[hover=true]:!bg-transparent !min-w-0 !w-auto !h-auto px-1", + selectorButton: heroUiAutocompleteSelectorButtonClassName, }} selectorButtonProps={{ disableRipple: true }} inputProps={{ classNames: { inputWrapper: cn( - "bg-tertiary border border-[var(--oh-border-input)] h-10 w-full min-w-0 rounded-sm p-2 placeholder:text-tertiary-alt", + formControlSettingsFieldClassName, inputWrapperClassName, ), input: inputClassName, diff --git a/src/components/features/settings/settings-input.tsx b/src/components/features/settings/settings-input.tsx index 0a4ee2370..1d35bcb9b 100644 --- a/src/components/features/settings/settings-input.tsx +++ b/src/components/features/settings/settings-input.tsx @@ -1,5 +1,6 @@ import { forwardRef } from "react"; import { cn } from "#/utils/utils"; +import { formControlSettingsFieldClassName } from "#/utils/form-control-classes"; import { OptionalTag } from "./optional-tag"; interface SettingsInputProps { @@ -106,8 +107,8 @@ export const SettingsInput = forwardRef( aria-describedby={errorId ?? ariaDescribedBy} aria-invalid={!!error || ariaInvalid} className={cn( - "bg-tertiary border border-[var(--oh-border-input)] h-10 w-full min-w-0 rounded-sm p-2 placeholder:text-tertiary-alt", - "disabled:bg-[var(--oh-surface-raised)] disabled:border-[var(--oh-border-subtle)] disabled:cursor-not-allowed", + formControlSettingsFieldClassName, + "disabled:bg-[var(--oh-surface-raised)] disabled:border-[var(--oh-border-subtle)]", error && "border-red-500", inputClassName, )} diff --git a/src/components/features/settings/settings-nav-link.tsx b/src/components/features/settings/settings-nav-link.tsx index 74528fa9f..209866a4d 100644 --- a/src/components/features/settings/settings-nav-link.tsx +++ b/src/components/features/settings/settings-nav-link.tsx @@ -4,6 +4,7 @@ import { cn } from "#/utils/utils"; import { Typography } from "#/ui/typography"; import { I18nKey } from "#/i18n/declaration"; import { SettingsNavItem } from "#/constants/settings-nav"; +import { navInteractiveTransitionClassName } from "#/components/features/sidebar/sidebar-layout"; interface SettingsNavLinkProps { item: SettingsNavItem; @@ -55,20 +56,21 @@ export function SettingsNavLink({ onClick={onClick} className={({ isActive }) => cn( - "group flex items-center gap-3 p-1 sm:px-3.5 sm:py-2 rounded transition-all duration-200", + "group flex items-center gap-3 p-1 sm:px-3.5 sm:py-2 rounded", + navInteractiveTransitionClassName, isActive ? "bg-tertiary" : "hover:bg-[var(--oh-surface-raised)]", isActive ? "[&_*]:text-white" : "", ) } > - + {icon}
    {t(text as I18nKey)} diff --git a/src/components/features/settings/settings-navigation.tsx b/src/components/features/settings/settings-navigation.tsx index 315c39b56..02bb33c08 100644 --- a/src/components/features/settings/settings-navigation.tsx +++ b/src/components/features/settings/settings-navigation.tsx @@ -9,6 +9,7 @@ import { SettingsNavHeader } from "./settings-nav-header"; import { SettingsNavDivider } from "./settings-nav-divider"; import { SettingsNavLink } from "./settings-nav-link"; import { SidebarNavLink } from "#/components/features/sidebar/sidebar-nav-link"; +import { navInteractiveTransitionClassName } from "#/components/features/sidebar/sidebar-layout"; import { BackendSyncedSettingsBadge } from "#/components/features/settings/backend-synced-settings-badge"; interface SettingsNavigationProps { @@ -111,7 +112,10 @@ export function SettingsMobileDrawer({ -
    - ) : ( - <> - - {showCollapseToggle ? ( - - ) : null} - {showMobileCloseButton ? ( - - ) : null} - - )} + ) : null} +
    + {!collapsed && showCollapseToggle ? ( + + ) : null} + {!collapsed && showMobileCloseButton ? ( + + ) : null}