mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-01 12:02:05 +00:00
Merge pull request #55470 from NousResearch/bb/desktop-button-consistency
refactor(desktop): formalize row-as-button primitive (RowButton)
This commit is contained in:
commit
a1b6e7eadc
7 changed files with 70 additions and 15 deletions
|
|
@ -16,6 +16,7 @@ import {
|
|||
PaginationNext,
|
||||
PaginationPrevious
|
||||
} from '@/components/ui/pagination'
|
||||
import { RowButton } from '@/components/ui/row-button'
|
||||
import { TextTab, TextTabMeta } from '@/components/ui/text-tab'
|
||||
import { Tip } from '@/components/ui/tooltip'
|
||||
import { getSessionMessages, listAllProfileSessions } from '@/hermes'
|
||||
|
|
@ -761,13 +762,12 @@ function ArtifactCellAction({
|
|||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
<RowButton
|
||||
className="flex h-full w-full min-w-0 items-center gap-2 px-2.5 py-1.5 text-left text-[length:var(--conversation-caption-font-size)] leading-(--conversation-caption-line-height) font-normal text-(--ui-text-secondary) no-underline underline-offset-4 decoration-current/20 transition-colors hover:text-foreground hover:underline"
|
||||
onClick={onClick}
|
||||
type="button"
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
</RowButton>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import type * as React from 'react'
|
||||
|
||||
import { Codicon } from '@/components/ui/codicon'
|
||||
import { RowButton } from '@/components/ui/row-button'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
// Shared, content-agnostic sidebar chrome — used by both the flat session
|
||||
|
|
@ -64,7 +65,7 @@ export function SidebarRowCluster({ className, ...props }: React.ComponentProps<
|
|||
|
||||
/** Session row main tap target. */
|
||||
export function SidebarRowBody({ className, ...props }: React.ComponentProps<'button'>) {
|
||||
return <button className={cn(rowInset, 'bg-transparent text-left', className)} type="button" {...props} />
|
||||
return <RowButton className={cn(rowInset, 'bg-transparent text-left', className)} {...props} />
|
||||
}
|
||||
|
||||
/** Tappable label — underline/truncate live on the inner span, not the button. */
|
||||
|
|
@ -75,9 +76,9 @@ export function SidebarRowLink({
|
|||
...props
|
||||
}: React.ComponentProps<'button'> & { labelClassName?: string }) {
|
||||
return (
|
||||
<button className={cn('min-w-0 shrink bg-transparent p-0 text-left', className)} type="button" {...props}>
|
||||
<RowButton className={cn('min-w-0 shrink bg-transparent p-0 text-left', className)} {...props}>
|
||||
<span className={cn(rowLabel, labelClassName)}>{children}</span>
|
||||
</button>
|
||||
</RowButton>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import type { ReactNode } from 'react'
|
|||
import { Button } from '@/components/ui/button'
|
||||
import { Codicon } from '@/components/ui/codicon'
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'
|
||||
import { RowButton } from '@/components/ui/row-button'
|
||||
import { SearchField } from '@/components/ui/search-field'
|
||||
import { translateNow } from '@/i18n'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
|
@ -162,10 +163,9 @@ export function PanelListRow({
|
|||
)}
|
||||
data-panel-row={rowKey}
|
||||
>
|
||||
<button
|
||||
<RowButton
|
||||
className="flex h-full min-w-0 flex-1 items-center gap-2 rounded-md pl-2 pr-1 text-left"
|
||||
onClick={onSelect}
|
||||
type="button"
|
||||
>
|
||||
{lead ??
|
||||
(dotClassName ? (
|
||||
|
|
@ -174,7 +174,7 @@ export function PanelListRow({
|
|||
<Codicon className="shrink-0 text-muted-foreground/55" name={icon} size="0.85rem" />
|
||||
) : null)}
|
||||
<span className="min-w-0 flex-1 truncate font-medium text-foreground/85">{title}</span>
|
||||
</button>
|
||||
</RowButton>
|
||||
{meta ? <span className="shrink-0 pr-2 text-[0.62rem] tabular-nums text-muted-foreground/45">{meta}</span> : null}
|
||||
{menu ? <div className="shrink-0 pr-1">{menu}</div> : null}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import {
|
|||
sortProviders
|
||||
} from '@/components/desktop-onboarding-overlay'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { RowButton } from '@/components/ui/row-button'
|
||||
import { SearchField } from '@/components/ui/search-field'
|
||||
import { disconnectOAuthProvider, listOAuthProviders } from '@/hermes'
|
||||
import { useI18n } from '@/i18n'
|
||||
|
|
@ -237,7 +238,7 @@ function ConnectedProviderRow({
|
|||
|
||||
return (
|
||||
<div className="group grid grid-cols-[minmax(0,1fr)_auto] items-center gap-1 rounded-[6px] transition-colors hover:bg-(--ui-control-hover-background)">
|
||||
<button className="min-w-0 px-3 py-2.5 text-left" onClick={() => onSelect(provider)} type="button">
|
||||
<RowButton className="min-w-0 px-3 py-2.5 text-left" onClick={() => onSelect(provider)}>
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<span className="truncate text-[length:var(--conversation-text-font-size)] font-semibold">{title}</span>
|
||||
<span className="inline-flex shrink-0 items-center gap-1 bg-primary/10 px-2 py-0.5 text-xs font-medium text-primary">
|
||||
|
|
@ -251,7 +252,7 @@ function ConnectedProviderRow({
|
|||
{provider.flow === 'external' ? copy.removeExternalGeneric(title) : copy.removeKeyManaged(title)}
|
||||
</p>
|
||||
)}
|
||||
</button>
|
||||
</RowButton>
|
||||
<div className="flex items-center gap-1 pr-2">
|
||||
<Trail className="size-4 text-muted-foreground transition group-hover:text-foreground" />
|
||||
{canDisconnect && (
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { Codicon } from '@/components/ui/codicon'
|
|||
import { ErrorIcon } from '@/components/ui/error-state'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Loader } from '@/components/ui/loader'
|
||||
import { RowButton } from '@/components/ui/row-button'
|
||||
import { getGlobalModelOptions } from '@/hermes'
|
||||
import { useI18n } from '@/i18n'
|
||||
import { Check, ChevronDown, ChevronLeft, ChevronRight, ExternalLink, KeyRound, Loader2, Terminal } from '@/lib/icons'
|
||||
|
|
@ -575,13 +576,13 @@ export function KeyProviderRow({ onClick }: { onClick: () => void }) {
|
|||
const { t } = useI18n()
|
||||
|
||||
return (
|
||||
<button className={PROVIDER_ROW_CLASS} onClick={onClick} type="button">
|
||||
<RowButton className={PROVIDER_ROW_CLASS} onClick={onClick}>
|
||||
<div className="min-w-0">
|
||||
<span className="text-[length:var(--conversation-text-font-size)] font-semibold">OpenRouter</span>
|
||||
<p className="mt-1 text-xs leading-5 text-muted-foreground">{t.onboarding.openRouterPitch}</p>
|
||||
</div>
|
||||
<ChevronRight className="size-4 text-muted-foreground transition group-hover:text-foreground" />
|
||||
</button>
|
||||
</RowButton>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -597,7 +598,7 @@ export function ProviderRow({
|
|||
const Trail = provider.flow === 'external' ? Terminal : ChevronRight
|
||||
|
||||
return (
|
||||
<button className={PROVIDER_ROW_CLASS} onClick={() => onSelect(provider)} type="button">
|
||||
<RowButton className={PROVIDER_ROW_CLASS} onClick={() => onSelect(provider)}>
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[length:var(--conversation-text-font-size)] font-semibold">
|
||||
|
|
@ -608,7 +609,7 @@ export function ProviderRow({
|
|||
<p className="mt-1 text-xs leading-5 text-muted-foreground">{t.onboarding.flowSubtitles[provider.flow]}</p>
|
||||
</div>
|
||||
<Trail className="size-4 text-muted-foreground transition group-hover:text-foreground" />
|
||||
</button>
|
||||
</RowButton>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
39
apps/desktop/src/components/ui/row-button.test.tsx
Normal file
39
apps/desktop/src/components/ui/row-button.test.tsx
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import { cleanup, render } from '@testing-library/react'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { RowButton } from './row-button'
|
||||
|
||||
afterEach(cleanup)
|
||||
|
||||
describe('RowButton', () => {
|
||||
it('renders a real <button> with type=button and the row-button slot', () => {
|
||||
const { getByText } = render(<RowButton>Row</RowButton>)
|
||||
const el = getByText('Row')
|
||||
|
||||
expect(el.tagName).toBe('BUTTON')
|
||||
expect(el.getAttribute('type')).toBe('button')
|
||||
expect(el.getAttribute('data-slot')).toBe('row-button')
|
||||
})
|
||||
|
||||
it('imposes no styling of its own — only the caller class is applied', () => {
|
||||
const onClick = vi.fn()
|
||||
|
||||
const { getByText } = render(
|
||||
<RowButton className="custom-row" onClick={onClick}>
|
||||
Hit
|
||||
</RowButton>
|
||||
)
|
||||
|
||||
const el = getByText('Hit')
|
||||
|
||||
expect(el.className).toBe('custom-row')
|
||||
el.click()
|
||||
expect(onClick).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('allows the native button type to be overridden', () => {
|
||||
const { getByText } = render(<RowButton type="submit">Go</RowButton>)
|
||||
|
||||
expect(getByText('Go').getAttribute('type')).toBe('submit')
|
||||
})
|
||||
})
|
||||
13
apps/desktop/src/components/ui/row-button.tsx
Normal file
13
apps/desktop/src/components/ui/row-button.tsx
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import * as React from 'react'
|
||||
|
||||
/**
|
||||
* A full-row / region click target rendered as a real `<button>`: bakes in
|
||||
* `type="button"` + a stable `data-slot`, imposes no styling (callers keep their
|
||||
* own layout classes, so nothing changes visually). Use for row/region targets;
|
||||
* use `Button` for ordinary compact actions.
|
||||
*/
|
||||
function RowButton({ className, type = 'button', ...props }: React.ComponentProps<'button'>) {
|
||||
return <button className={className} data-slot="row-button" type={type} {...props} />
|
||||
}
|
||||
|
||||
export { RowButton }
|
||||
Loading…
Add table
Add a link
Reference in a new issue