hermes-agent/apps/desktop/src/components/ui/split-button.tsx
Brooklyn Nicholson 62fe9fd101 style(desktop,tui): fix all lint/type/formatting issues
Bring apps/desktop and ui-tui to a clean state for typecheck, eslint,
and prettier:

- Run prettier across both trees (printWidth/wrap drift; prettier is not
  CI-enforced for these JS projects, so main had accumulated drift).
- Apply eslint --fix for padding-line-between-statements and perfectionist
  import/export sorting.
- Manual fixes for non-auto-fixable rules:
  - remove unused node:net import in electron/main.cjs (uses Electron net)
  - replace inline `typeof import(...)` annotations with top-level
    `import type * as EnvModule` in two ui-tui test files
  - scoped eslint-disable no-control-regex on intentional sentinel/ANSI
    regexes (mathUnicode.ts, text.ts)
  - resolve react-hooks/exhaustive-deps per-case: correct swapped/missing
    deps, collapse redundant session.* members, and justified disables on
    settings mount-only data-load effects to preserve run-once behavior

No behavior changes; test pass/fail counts are unchanged from the main
baseline.
2026-06-26 01:04:33 -05:00

98 lines
3 KiB
TypeScript

import type { VariantProps } from 'class-variance-authority'
import type { ReactNode } from 'react'
import { Codicon } from '@/components/ui/codicon'
import { cn } from '@/lib/utils'
import type { buttonVariants } from './button'
import { Button } from './button'
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from './dropdown-menu'
export interface SplitButtonAction {
id: string
label: string
icon?: ReactNode
}
interface SplitButtonProps {
actions: SplitButtonAction[]
/** The id of the action the primary button runs (the user's current default). */
value: string
/** Picking from the menu changes the default (so the next primary click repeats it). */
onValueChange: (id: string) => void
/** Run an action by id (primary click or menu pick both call this). */
onTrigger: (id: string) => void
disabled?: boolean
className?: string
/** Icon shown on the primary button only (e.g. a ✓ for Commit). */
primaryIcon?: ReactNode
variant?: VariantProps<typeof buttonVariants>['variant']
size?: VariantProps<typeof buttonVariants>['size']
}
/**
* A primary action fused to a caret that opens alternates — VS Code's
* Commit / Commit & Push pattern. The primary button runs `value`; picking a
* menu item runs it AND makes it the new default, so the control adapts to how
* the user works without a separate settings toggle.
*/
export function SplitButton({
actions,
value,
onValueChange,
onTrigger,
disabled,
className,
primaryIcon,
variant = 'secondary',
size = 'sm'
}: SplitButtonProps) {
const active = actions.find(action => action.id === value) ?? actions[0]
if (!active) {
return null
}
return (
<div className={cn('inline-flex min-w-0', className)}>
<Button
className="min-w-0 flex-1 rounded-r-none"
disabled={disabled}
onClick={() => onTrigger(active.id)}
size={size}
variant={variant}
>
{primaryIcon ?? active.icon}
<span className="truncate">{active.label}</span>
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
aria-label="More actions"
className="rounded-l-none border-l border-current/25 px-2"
disabled={disabled}
size={size}
variant={variant}
>
<Codicon name="chevron-down" size="0.8rem" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="min-w-44">
{actions.map(action => (
<DropdownMenuItem
key={action.id}
onSelect={() => {
onValueChange(action.id)
onTrigger(action.id)
}}
>
{action.icon}
<span className="flex-1 truncate">{action.label}</span>
{action.id === value && <Codicon className="text-(--ui-text-tertiary)" name="check" size="0.75rem" />}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</div>
)
}