Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/bright-dogs-grin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@inkeep/agents-manage-ui": patch
---

Add InputGroup compound component and use it in ProjectSwitcher search input
13 changes: 7 additions & 6 deletions agents-manage-ui/src/components/sidebar-nav/project-switcher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { Input } from '@/components/ui/input';
import { InputGroup, InputGroupAddon, InputGroupInput } from '@/components/ui/input-group';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The checked-out file shows this import ended up on line 20, after @/lib/query/projects. Biome's organizeImports expects @/components/ui/input-group to sort before @/components/ui/sidebar. Run pnpm format to auto-fix.

import { SidebarMenuButton, useSidebar } from '@/components/ui/sidebar';
import { Skeleton } from '@/components/ui/skeleton';
import { useIsOrgAdmin } from '@/hooks/use-is-org-admin';
Expand Down Expand Up @@ -92,17 +92,18 @@ export const ProjectSwitcher: FC = () => {
align="end"
sideOffset={4}
>
<div className="flex items-center gap-2 px-2 py-1.5">
<Search className="size-4 shrink-0 text-muted-foreground" />
<Input
<InputGroup className="h-7 border-0 has-[[data-slot=input-group-control]:focus-visible]:ring-0 shadow-none">
<InputGroupInput
aria-label="Search projects"
placeholder="Search projects..."
value={search}
onChange={(e) => setSearch(e.target.value)}
onKeyDown={(e) => e.stopPropagation()}
className="h-7 border-none shadow-none focus-visible:ring-0 px-0"
/>
</div>
<InputGroupAddon>
<Search />
</InputGroupAddon>
</InputGroup>
<DropdownMenuSeparator />
<div className="overflow-y-auto max-h-[200px]">
{filteredProjects.length === 0 ? (
Expand Down
158 changes: 158 additions & 0 deletions agents-manage-ui/src/components/ui/input-group.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
'use client';

import { cva, type VariantProps } from 'class-variance-authority';
import type * as React from 'react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { cn } from '@/lib/utils';

function InputGroup({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="input-group"
role="group"
className={cn(
'group/input-group relative flex w-full items-center rounded-md border border-input shadow-xs transition-[color,box-shadow] outline-none dark:bg-input/30',
'h-9 min-w-0 has-[>textarea]:h-auto',

// Variants based on alignment.
'has-[>[data-align=inline-start]]:[&>input]:pl-2',
'has-[>[data-align=inline-end]]:[&>input]:pr-2',
'has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3',
'has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3',

// Focus state.
'has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-[3px] has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50',

// Error state.
'has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:ring-destructive/20 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40',

className
)}
{...props}
/>
);
}

const inputGroupAddonVariants = cva(
"flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium text-muted-foreground select-none group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-4",
{
variants: {
align: {
'inline-start': 'order-first pl-3 has-[>button]:ml-[-0.45rem] has-[>kbd]:ml-[-0.35rem]',
'inline-end': 'order-last pr-3 has-[>button]:mr-[-0.45rem] has-[>kbd]:mr-[-0.35rem]',
'block-start':
'order-first w-full justify-start px-3 pt-3 group-has-[>input]/input-group:pt-2.5 [.border-b]:pb-3',
'block-end':
'order-last w-full justify-start px-3 pb-3 group-has-[>input]/input-group:pb-2.5 [.border-t]:pt-3',
},
},
defaultVariants: {
align: 'inline-start',
},
}
);

function InputGroupAddon({
className,
align = 'inline-start',
...props
}: React.ComponentProps<'div'> & VariantProps<typeof inputGroupAddonVariants>) {
return (
// biome-ignore lint/a11y/useKeyWithClickEvents: use official shadcn example, todo: maybe refactor in the future
<div
role="group"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: InputGroupAddon uses role="group" while its parent InputGroup already uses role="group". Nested ARIA groups imply a sub-grouping of related controls, but this element is a decorative icon container. Consider dropping role here or using role="presentation" to avoid misleading assistive technology.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Minor: Redundant role="group" on addon

Issue: InputGroupAddon has role="group" which is semantically incorrect for this element. The addon is not a grouping of interactive elements — it contains visual decorations or buttons.

Why: Using incorrect ARIA roles can confuse assistive technology users about the element's purpose. A role="group" suggests a collection of related form controls, but this addon just contains an icon or button. The parent InputGroup already has role="group" which is appropriate for the entire input group.

Fix: Remove role="group" from InputGroupAddon:

Suggested change
role="group"
<div

Refs:

data-slot="input-group-addon"
data-align={align}
className={cn(inputGroupAddonVariants({ align }), className)}
onClick={(e) => {
if ((e.target as HTMLElement).closest('button')) {
return;
}
e.currentTarget.parentElement?.querySelector('input')?.focus();
}}
{...props}
/>
);
}

const inputGroupButtonVariants = cva('flex items-center gap-2 text-sm shadow-none', {
variants: {
size: {
xs: "h-6 gap-1 rounded-[calc(var(--radius)-5px)] px-2 has-[>svg]:px-2 [&>svg:not([class*='size-'])]:size-3.5",
sm: 'h-8 gap-1.5 rounded-md px-2.5 has-[>svg]:px-2.5',
'icon-xs': 'size-6 rounded-[calc(var(--radius)-5px)] p-0 has-[>svg]:p-0',
'icon-sm': 'size-8 p-0 has-[>svg]:p-0',
},
},
defaultVariants: {
size: 'xs',
},
});

function InputGroupButton({
className,
type = 'button',
variant = 'ghost',
size = 'xs',
...props
}: Omit<React.ComponentProps<typeof Button>, 'size'> &
VariantProps<typeof inputGroupButtonVariants>) {
return (
<Button
type={type}
data-size={size}
variant={variant}
className={cn(inputGroupButtonVariants({ size }), className)}
{...props}
/>
);
}

function InputGroupText({ className, ...props }: React.ComponentProps<'span'>) {
return (
<span
className={cn(
"flex items-center gap-2 text-sm text-muted-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
);
Comment on lines +113 to +122
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💭 Consider: Add data-slot attribute for consistency

Issue: InputGroupText is missing a data-slot attribute unlike most other UI subcomponents in this codebase.

Why: Most compound component parts use data-slot for CSS targeting and component identification (e.g., CardTitle, AlertTitle, DialogTitle, FormItem). While ButtonGroupText also lacks data-slot (making this not unprecedented), the majority pattern includes it.

Fix: Consider adding data-slot="input-group-text" to the span element for consistency:

Suggested change
function InputGroupText({ className, ...props }: React.ComponentProps<'span'>) {
return (
<span
className={cn(
"flex items-center gap-2 text-sm text-muted-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
);
function InputGroupText({ className, ...props }: React.ComponentProps<'span'>) {
return (
<span
data-slot="input-group-text"
className={cn(
"flex items-center gap-2 text-sm text-muted-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
);
}

Refs:

}

function InputGroupInput({ className, ...props }: React.ComponentProps<'input'>) {
return (
<Input
data-slot="input-group-control"
className={cn(
'flex-1 rounded-none border-0 bg-transparent shadow-none focus-visible:ring-0 dark:bg-transparent',
className
)}
{...props}
/>
);
}

function InputGroupTextarea({ className, ...props }: React.ComponentProps<'textarea'>) {
return (
<Textarea
data-slot="input-group-control"
className={cn(
'flex-1 resize-none rounded-none border-0 bg-transparent py-3 shadow-none focus-visible:ring-0 dark:bg-transparent',
className
)}
{...props}
/>
);
}

export {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupText,
InputGroupInput,
InputGroupTextarea,
};
Loading