mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 08:02:28 +00:00
Add Project modal: roving keyboard selection (#5052)
* Make Add Project highlight a roving keyboard selection The Add Project modal previously rendered Browse folder as a permanently filled "primary" card with a static ⏎ chip. Turn that white fill + ⏎ chip into a roving selection indicator driven by keyboard focus: Browse starts selected (it is autofocused on open), and Tab or ↑/↓ move the highlight — and the ⏎ chip — to whichever action is focused, so Enter's target is always the highlighted row. Also flatten the unselected Browse card's surface to bg-background so it matches the secondary rows instead of showing the outline variant's lighter tinted/shadowed surface. ShortcutKeyCombo gains an optional keyCapClassName so the ⏎ chip can tint itself for the filled surface. Co-authored-by: Orca <help@stably.ai> * Drop focus ring on selected Browse card so it stays borderless The primary Browse card uses the Button component, whose base styles always draw a focus-visible border + ring. Because Browse is autofocused-and-selected on open, that ring rendered as a border on only the top card, unlike the filled secondary rows. Suppress the ring on the selected state (the fill + ⏎ chip already indicate focus, since focus drives selection) and add a transparent border to hold the box size steady across the outline↔default swap. Co-authored-by: Orca <help@stably.ai> * Fix add project action selection accessibility Co-authored-by: Orca <help@stably.ai> * Refine add project action outlines Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
@@ -1,9 +1,14 @@
|
||||
import React from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
function KeyCap({ label }: { label: string }): React.JSX.Element {
|
||||
function KeyCap({ label, className }: { label: string; className?: string }): React.JSX.Element {
|
||||
return (
|
||||
<span className="inline-flex min-w-6 items-center justify-center rounded border border-border/80 bg-secondary/70 px-1.5 py-0.5 text-xs font-medium text-muted-foreground shadow-sm">
|
||||
<span
|
||||
className={cn(
|
||||
'inline-flex min-w-6 items-center justify-center rounded border border-border/80 bg-secondary/70 px-1.5 py-0.5 text-xs font-medium text-muted-foreground shadow-sm',
|
||||
className
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
)
|
||||
@@ -13,12 +18,15 @@ type ShortcutKeyComboProps = {
|
||||
keys: string[]
|
||||
className?: string
|
||||
separatorClassName?: string
|
||||
// Override cap colors when chips sit on a non-default surface (e.g. a filled primary card).
|
||||
keyCapClassName?: string
|
||||
}
|
||||
|
||||
export function ShortcutKeyCombo({
|
||||
keys,
|
||||
className,
|
||||
separatorClassName
|
||||
separatorClassName,
|
||||
keyCapClassName
|
||||
}: ShortcutKeyComboProps): React.JSX.Element {
|
||||
const isMac = navigator.userAgent.includes('Mac')
|
||||
|
||||
@@ -26,7 +34,7 @@ export function ShortcutKeyCombo({
|
||||
<span className={cn('inline-flex items-center gap-1', className)}>
|
||||
{keys.map((key, index) => (
|
||||
<React.Fragment key={`${key}-${index}`}>
|
||||
<KeyCap label={key} />
|
||||
<KeyCap label={key} className={keyCapClassName} />
|
||||
{/* Why: Orca renders Mac shortcuts as adjacent glyphs, but Windows/Linux
|
||||
shortcuts read more naturally with explicit "+" separators. */}
|
||||
{!isMac && index < keys.length - 1 ? (
|
||||
|
||||
@@ -51,7 +51,17 @@ function renderServerPathStartStep(runtimeEnvironmentId: string | null): string
|
||||
)
|
||||
}
|
||||
|
||||
async function renderLocalStartStepDom(isSshLikely: boolean): Promise<{
|
||||
type LocalStartStepDomOptions = {
|
||||
isAdding?: boolean
|
||||
addProjectBusyLabel?: string | null
|
||||
nestedScanInProgress?: boolean
|
||||
nestedScanId?: string | null
|
||||
}
|
||||
|
||||
async function renderLocalStartStepDom(
|
||||
isSshLikely: boolean,
|
||||
options: LocalStartStepDomOptions = {}
|
||||
): Promise<{
|
||||
container: HTMLDivElement
|
||||
root: Root
|
||||
}> {
|
||||
@@ -61,19 +71,21 @@ async function renderLocalStartStepDom(isSshLikely: boolean): Promise<{
|
||||
|
||||
await act(async () => {
|
||||
root.render(
|
||||
<AddRepoLocalStartStep
|
||||
repoCount={1}
|
||||
isSshLikely={isSshLikely}
|
||||
isAdding={false}
|
||||
addProjectBusyLabel={null}
|
||||
nestedScanInProgress={false}
|
||||
nestedScanId={null}
|
||||
onBrowse={vi.fn()}
|
||||
onOpenCloneStep={vi.fn()}
|
||||
onOpenRemoteStep={vi.fn()}
|
||||
onOpenCreateStep={vi.fn()}
|
||||
onStopNestedScan={vi.fn()}
|
||||
/>
|
||||
<TooltipProvider>
|
||||
<AddRepoLocalStartStep
|
||||
repoCount={1}
|
||||
isSshLikely={isSshLikely}
|
||||
isAdding={options.isAdding ?? false}
|
||||
addProjectBusyLabel={options.addProjectBusyLabel ?? null}
|
||||
nestedScanInProgress={options.nestedScanInProgress ?? false}
|
||||
nestedScanId={options.nestedScanId ?? null}
|
||||
onBrowse={vi.fn()}
|
||||
onOpenCloneStep={vi.fn()}
|
||||
onOpenRemoteStep={vi.fn()}
|
||||
onOpenCreateStep={vi.fn()}
|
||||
onStopNestedScan={vi.fn()}
|
||||
/>
|
||||
</TooltipProvider>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -120,7 +132,7 @@ describe('AddRepoLocalStartStep', () => {
|
||||
expect(markup).toContain('Clone from URL')
|
||||
expect(markup).toContain('Remote project')
|
||||
expect(markup).toContain('Create new project')
|
||||
expect(markup).toContain('Or add from')
|
||||
expect(markup).toContain('Other ways to add')
|
||||
expect(markup).not.toContain('More options')
|
||||
})
|
||||
|
||||
@@ -182,6 +194,108 @@ describe('AddRepoLocalStartStep', () => {
|
||||
root.unmount()
|
||||
})
|
||||
})
|
||||
|
||||
it('marks the autofocused Browse action as selected with the ⏎ chip', async () => {
|
||||
const { container, root } = await renderLocalStartStepDom(false)
|
||||
|
||||
expect(findButton(container, 'Browse folder').textContent).toContain('⏎')
|
||||
expect(findButton(container, 'Clone from URL').textContent).not.toContain('⏎')
|
||||
|
||||
await act(async () => {
|
||||
root.unmount()
|
||||
})
|
||||
})
|
||||
|
||||
it('moves the ⏎ selection to whichever action receives focus', async () => {
|
||||
const { container, root } = await renderLocalStartStepDom(false)
|
||||
const cloneButton = findButton(container, 'Clone from URL')
|
||||
|
||||
await act(async () => {
|
||||
cloneButton.focus()
|
||||
})
|
||||
|
||||
expect(findButton(container, 'Clone from URL').textContent).toContain('⏎')
|
||||
expect(findButton(container, 'Browse folder').textContent).not.toContain('⏎')
|
||||
|
||||
await act(async () => {
|
||||
root.unmount()
|
||||
})
|
||||
})
|
||||
|
||||
it('clears the ⏎ selection when focus leaves the action list', async () => {
|
||||
const { container, root } = await renderLocalStartStepDom(false)
|
||||
const outsideButton = document.createElement('button')
|
||||
document.body.appendChild(outsideButton)
|
||||
|
||||
await act(async () => {
|
||||
outsideButton.focus()
|
||||
})
|
||||
|
||||
expect(document.activeElement).toBe(outsideButton)
|
||||
expect(findButton(container, 'Browse folder').textContent).not.toContain('⏎')
|
||||
expect(findButton(container, 'Clone from URL').textContent).not.toContain('⏎')
|
||||
|
||||
await act(async () => {
|
||||
root.unmount()
|
||||
})
|
||||
})
|
||||
|
||||
it('does not show an ⏎ selection while add actions are busy', async () => {
|
||||
const { container, root } = await renderLocalStartStepDom(false, {
|
||||
isAdding: true,
|
||||
addProjectBusyLabel: 'Scanning repositories',
|
||||
nestedScanInProgress: true,
|
||||
nestedScanId: 'scan-1'
|
||||
})
|
||||
const stopScanButton = container.querySelector<HTMLButtonElement>(
|
||||
'button[aria-label="Stop scan"]'
|
||||
)
|
||||
|
||||
await act(async () => {
|
||||
stopScanButton?.focus()
|
||||
})
|
||||
|
||||
expect(document.activeElement).toBe(stopScanButton)
|
||||
expect(findButton(container, 'Browse folder').textContent).not.toContain('⏎')
|
||||
expect(findButton(container, 'Clone from URL').textContent).not.toContain('⏎')
|
||||
|
||||
await act(async () => {
|
||||
root.unmount()
|
||||
})
|
||||
})
|
||||
|
||||
it('hides the visual ⏎ chip from assistive technology', async () => {
|
||||
const { container, root } = await renderLocalStartStepDom(false)
|
||||
const browseButton = findButton(container, 'Browse folder')
|
||||
const enterChip = Array.from(browseButton.querySelectorAll('[aria-hidden="true"]')).find(
|
||||
(entry) => entry.textContent?.includes('⏎')
|
||||
)
|
||||
|
||||
expect(enterChip).toBeTruthy()
|
||||
|
||||
await act(async () => {
|
||||
root.unmount()
|
||||
})
|
||||
})
|
||||
|
||||
it('roves selection down the action list with the ArrowDown key', async () => {
|
||||
const { container, root } = await renderLocalStartStepDom(false)
|
||||
|
||||
await act(async () => {
|
||||
findButton(container, 'Browse folder').dispatchEvent(
|
||||
new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true })
|
||||
)
|
||||
})
|
||||
|
||||
// ArrowDown from Browse moves focus — and the ⏎ chip — to the first secondary action.
|
||||
const firstSecondary = findButton(container, 'Clone from URL')
|
||||
expect(document.activeElement).toBe(firstSecondary)
|
||||
expect(firstSecondary.textContent).toContain('⏎')
|
||||
|
||||
await act(async () => {
|
||||
root.unmount()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('AddRepoServerPathStartStep', () => {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useEffect, useRef, type ComponentType, type Ref } from 'react'
|
||||
import { useEffect, useRef, useState, type ComponentType, type Ref } from 'react'
|
||||
import { CircleStop, Loader2 } from 'lucide-react'
|
||||
import { DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { ShortcutKeyCombo } from '@/components/ShortcutKeyCombo'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { getAddRepoLocalStartActions } from './add-repo-local-start-actions'
|
||||
@@ -32,8 +33,14 @@ function AddRepoNestedScanProgressNotice({
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
className="group text-muted-foreground hover:bg-destructive/10 hover:text-destructive focus-visible:bg-destructive/10 focus-visible:text-destructive focus-visible:ring-destructive/40"
|
||||
aria-label={translate("auto.components.sidebar.AddRepoStartSteps.9906cae183", "Stop scan")}
|
||||
title={translate("auto.components.sidebar.AddRepoStartSteps.69ea7f8dc4", "Stop scanning")}
|
||||
aria-label={translate(
|
||||
'auto.components.sidebar.AddRepoStartSteps.9906cae183',
|
||||
'Stop scan'
|
||||
)}
|
||||
title={translate(
|
||||
'auto.components.sidebar.AddRepoStartSteps.69ea7f8dc4',
|
||||
'Stop scanning'
|
||||
)}
|
||||
onClick={onStopNestedScan}
|
||||
>
|
||||
<Loader2 className="size-3.5 animate-spin text-annotation-highlight group-hover:hidden group-focus-visible:hidden" />
|
||||
@@ -41,7 +48,11 @@ function AddRepoNestedScanProgressNotice({
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" sideOffset={4}>
|
||||
{translate("auto.components.sidebar.AddRepoStartSteps.d301db1c9a", "Scanning repositories. Click to stop.")}</TooltipContent>
|
||||
{translate(
|
||||
'auto.components.sidebar.AddRepoStartSteps.d301db1c9a',
|
||||
'Scanning repositories. Click to stop.'
|
||||
)}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -76,6 +87,7 @@ export function AddRepoLocalStartStep({
|
||||
onStopNestedScan
|
||||
}: AddRepoLocalStartStepProps): React.JSX.Element {
|
||||
const browseActionRef = useRef<HTMLButtonElement | null>(null)
|
||||
const actionsRef = useRef<HTMLDivElement | null>(null)
|
||||
const { primaryAction, secondaryActions } = getAddRepoLocalStartActions({
|
||||
isSshLikely,
|
||||
onBrowse,
|
||||
@@ -84,37 +96,91 @@ export function AddRepoLocalStartStep({
|
||||
onOpenCreateStep
|
||||
})
|
||||
|
||||
// The white fill + ⏎ chip is a roving selection indicator, not a fixed "primary" badge:
|
||||
// it follows keyboard focus so Enter always activates the highlighted action. Browse is
|
||||
// autofocused on open, so it starts selected; Tab and ↑/↓ move the highlight.
|
||||
const [selectedKind, setSelectedKind] = useState<string | null>(primaryAction.kind)
|
||||
|
||||
useEffect(() => {
|
||||
if (isAdding) {
|
||||
setSelectedKind(null)
|
||||
return
|
||||
}
|
||||
if (!isAdding) {
|
||||
browseActionRef.current?.focus()
|
||||
}
|
||||
}, [isAdding])
|
||||
|
||||
// ↑/↓ rove focus across the action buttons in visual order; focus drives the selection.
|
||||
const handleArrowNavigation = (event: React.KeyboardEvent<HTMLDivElement>): void => {
|
||||
if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp') {
|
||||
return
|
||||
}
|
||||
const buttons = Array.from(
|
||||
actionsRef.current?.querySelectorAll<HTMLButtonElement>('button[data-add-repo-action]') ?? []
|
||||
)
|
||||
if (buttons.length === 0) {
|
||||
return
|
||||
}
|
||||
const currentIndex = buttons.findIndex((button) => button === document.activeElement)
|
||||
const delta = event.key === 'ArrowDown' ? 1 : -1
|
||||
const nextIndex = (currentIndex + delta + buttons.length) % buttons.length
|
||||
event.preventDefault()
|
||||
buttons[nextIndex]?.focus()
|
||||
}
|
||||
|
||||
const handleActionsBlur = (event: React.FocusEvent<HTMLDivElement>): void => {
|
||||
if (!(event.relatedTarget instanceof HTMLButtonElement)) {
|
||||
setSelectedKind(null)
|
||||
return
|
||||
}
|
||||
if (!event.relatedTarget.matches('button[data-add-repo-action]')) {
|
||||
setSelectedKind(null)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{translate("auto.components.sidebar.AddRepoStartSteps.d13757911c", "Add a project")}</DialogTitle>
|
||||
<DialogTitle>
|
||||
{translate('auto.components.sidebar.AddRepoStartSteps.d13757911c', 'Add a project')}
|
||||
</DialogTitle>
|
||||
{repoCount === 0 ? (
|
||||
<DialogDescription>{translate("auto.components.sidebar.AddRepoStartSteps.acf895cb42", "Add a project to get started with Orca.")}</DialogDescription>
|
||||
<DialogDescription>
|
||||
{translate(
|
||||
'auto.components.sidebar.AddRepoStartSteps.acf895cb42',
|
||||
'Add a project to get started with Orca.'
|
||||
)}
|
||||
</DialogDescription>
|
||||
) : null}
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-3 pt-2">
|
||||
<div
|
||||
className="space-y-3 pt-2"
|
||||
ref={actionsRef}
|
||||
onBlur={handleActionsBlur}
|
||||
onKeyDown={handleArrowNavigation}
|
||||
>
|
||||
<AddRepoPrimaryStartAction
|
||||
icon={primaryAction.icon}
|
||||
title={primaryAction.title}
|
||||
description={primaryAction.description}
|
||||
disabled={isAdding}
|
||||
selected={selectedKind === primaryAction.kind}
|
||||
buttonRef={browseActionRef}
|
||||
onClick={primaryAction.onClick}
|
||||
onFocus={() => setSelectedKind(primaryAction.kind)}
|
||||
/>
|
||||
|
||||
{/* Keep secondary entry methods always visible so they stay discoverable without an extra click. */}
|
||||
{/* Label clarifies the lighter-weight rows are alternate entry methods, not lesser features. */}
|
||||
<div className="space-y-1.5">
|
||||
<p className="text-xs font-medium text-muted-foreground">{translate("auto.components.sidebar.AddRepoStartSteps.f3c96237ae", "Or add from…")}</p>
|
||||
{/* Match the primary card's surface (bg-background) so the group reads as the same family, not a recessed panel. */}
|
||||
<div className="overflow-hidden rounded-md border border-border/80 bg-background">
|
||||
<p className="text-xs font-medium uppercase tracking-wider text-muted-foreground">
|
||||
{translate('auto.components.sidebar.AddRepoStartSteps.87596c1446', 'Other ways to add')}
|
||||
</p>
|
||||
{/* Outline uses the `input` token (white-ish in dark mode) to match Browse's visible outline variant;
|
||||
primary-foreground is near-black in dark mode and rendered the border invisible. */}
|
||||
<div className="overflow-hidden rounded-md border border-input bg-background">
|
||||
{secondaryActions.map((action, index) => (
|
||||
<AddRepoSecondaryStartAction
|
||||
key={action.kind}
|
||||
@@ -122,7 +188,9 @@ export function AddRepoLocalStartStep({
|
||||
title={action.title}
|
||||
description={action.description}
|
||||
disabled={isAdding}
|
||||
selected={selectedKind === action.kind}
|
||||
onClick={action.onClick}
|
||||
onFocus={() => setSelectedKind(action.kind)}
|
||||
className={index === 0 ? '' : 'border-t border-border/70'}
|
||||
/>
|
||||
))}
|
||||
@@ -147,35 +215,73 @@ type AddRepoStartActionProps = {
|
||||
title: string
|
||||
description: string
|
||||
disabled: boolean
|
||||
// Selected = keyboard-focused: renders the white fill + trailing ⏎ chip so Enter's target is obvious.
|
||||
selected: boolean
|
||||
onClick: () => void
|
||||
onFocus: () => void
|
||||
buttonRef?: Ref<HTMLButtonElement>
|
||||
}
|
||||
|
||||
// Shared trailing chip so the ⏎ glyph travels with the selected action across primary and secondary rows.
|
||||
const AddRepoEnterChip = (): React.JSX.Element => (
|
||||
<span aria-hidden="true" className="shrink-0">
|
||||
<ShortcutKeyCombo
|
||||
keys={['⏎']}
|
||||
keyCapClassName="border-primary-foreground/20 bg-primary-foreground/10 text-primary-foreground/80"
|
||||
/>
|
||||
</span>
|
||||
)
|
||||
|
||||
const AddRepoPrimaryStartAction = ({
|
||||
icon: Icon,
|
||||
title,
|
||||
description,
|
||||
disabled,
|
||||
selected,
|
||||
onClick,
|
||||
onFocus,
|
||||
buttonRef
|
||||
}: AddRepoStartActionProps): React.JSX.Element => (
|
||||
// Filled white surface marks the selected action; when unselected the card reverts to a quiet
|
||||
// outline so the highlight reads as a moving selection, not a fixed badge. Inner tints switch to
|
||||
// primary-foreground only while filled, because muted tokens are tuned for the dark base surface.
|
||||
<Button
|
||||
ref={buttonRef}
|
||||
type="button"
|
||||
variant="outline"
|
||||
variant={selected ? 'default' : 'outline'}
|
||||
onClick={onClick}
|
||||
onFocus={onFocus}
|
||||
disabled={disabled}
|
||||
className="h-auto min-h-24 w-full justify-start gap-4 whitespace-normal border-border/80 bg-background px-4 py-4 text-left"
|
||||
data-add-repo-action
|
||||
className={cn(
|
||||
'h-auto min-h-[3.75rem] w-full justify-start gap-3 whitespace-normal px-3 py-2.5 text-left',
|
||||
// The subtle selected border keeps Browse and secondary rows feeling like one roving set.
|
||||
// Focus ring stays off because the fill + ⏎ chip already mark the focused action.
|
||||
selected
|
||||
? 'border border-primary-foreground/20 focus-visible:border-primary-foreground/30 focus-visible:ring-0'
|
||||
: 'bg-background shadow-none dark:bg-background'
|
||||
)}
|
||||
>
|
||||
<span className="grid size-11 shrink-0 place-items-center rounded-md text-foreground">
|
||||
<Icon className="size-5" />
|
||||
<span
|
||||
className={cn(
|
||||
'grid size-7 shrink-0 place-items-center rounded-md',
|
||||
selected ? 'bg-primary-foreground/10 text-primary-foreground' : 'text-foreground'
|
||||
)}
|
||||
>
|
||||
<Icon className="size-4" />
|
||||
</span>
|
||||
<span className="min-w-0">
|
||||
<span className="block text-sm font-semibold leading-5">{title}</span>
|
||||
<span className="mt-0.5 block text-xs font-normal leading-5 text-muted-foreground">
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block text-sm font-medium leading-5">{title}</span>
|
||||
<span
|
||||
className={cn(
|
||||
'mt-0.5 block text-xs font-normal leading-5',
|
||||
selected ? 'text-primary-foreground/70' : 'text-muted-foreground'
|
||||
)}
|
||||
>
|
||||
{description}
|
||||
</span>
|
||||
</span>
|
||||
{selected ? <AddRepoEnterChip /> : null}
|
||||
</Button>
|
||||
)
|
||||
|
||||
@@ -184,26 +290,54 @@ function AddRepoSecondaryStartAction({
|
||||
title,
|
||||
description,
|
||||
disabled,
|
||||
selected,
|
||||
onClick,
|
||||
onFocus,
|
||||
className
|
||||
}: AddRepoStartActionProps & { className?: string }): React.JSX.Element {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
data-add-repo-action
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
onFocus={onFocus}
|
||||
className={cn(
|
||||
'flex min-h-[3.25rem] w-full items-center gap-3 px-3 py-2.5 text-left transition-colors hover:bg-accent focus-visible:bg-accent focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-inset focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-default disabled:opacity-40',
|
||||
className
|
||||
'flex min-h-[3.25rem] w-full items-center gap-3 border border-transparent px-3 py-2.5 text-left transition-colors focus-visible:outline-none disabled:pointer-events-none disabled:cursor-default disabled:opacity-40',
|
||||
className,
|
||||
// Selected mirrors the primary card's filled white surface so the highlight moves between rows.
|
||||
selected
|
||||
? 'border-primary-foreground/30 bg-primary text-primary-foreground focus-visible:border-primary-foreground/40'
|
||||
: 'hover:bg-accent focus-visible:bg-accent focus-visible:ring-[3px] focus-visible:ring-inset focus-visible:ring-ring/50'
|
||||
)}
|
||||
>
|
||||
<span className="grid size-7 shrink-0 place-items-center rounded-md text-muted-foreground">
|
||||
<span
|
||||
className={cn(
|
||||
'grid size-7 shrink-0 place-items-center rounded-md',
|
||||
selected ? 'bg-primary-foreground/10 text-primary-foreground' : 'text-muted-foreground'
|
||||
)}
|
||||
>
|
||||
<Icon className="size-4" />
|
||||
</span>
|
||||
<span className="min-w-0">
|
||||
<span className="block text-sm font-medium leading-5 text-foreground">{title}</span>
|
||||
<span className="block text-xs leading-4 text-muted-foreground">{description}</span>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span
|
||||
className={cn(
|
||||
'block text-sm font-medium leading-5',
|
||||
selected ? 'text-primary-foreground' : 'text-foreground'
|
||||
)}
|
||||
>
|
||||
{title}
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
'block text-xs leading-4',
|
||||
selected ? 'text-primary-foreground/70' : 'text-muted-foreground'
|
||||
)}
|
||||
>
|
||||
{description}
|
||||
</span>
|
||||
</span>
|
||||
{selected ? <AddRepoEnterChip /> : null}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3030,7 +3030,7 @@
|
||||
"0f8aba944c": "Navigate to a directory and click Select to choose it."
|
||||
},
|
||||
"AddRepoStartSteps": {
|
||||
"f3c96237ae": "Or add from…",
|
||||
"87596c1446": "Other ways to add",
|
||||
"acf895cb42": "Add a project to get started with Orca.",
|
||||
"d13757911c": "Add a project",
|
||||
"d301db1c9a": "Scanning repositories. Click to stop.",
|
||||
|
||||
Reference in New Issue
Block a user